Merge pull request #4 from clayschaad/copilot/build-memory-game-for-dementia

Implement Memory Game for dementia patients with adaptive learning and auto-discovery image loading (German language)
This commit is contained in:
Claudio Schaad 2025-11-14 16:20:03 +01:00 committed by GitHub
commit f197f92306
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 60863 additions and 0 deletions

6
MemoryGame/App.razor Normal file
View file

@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>

View file

@ -0,0 +1,9 @@
@inherits LayoutComponentBase
<div class="page">
<main>
<article class="content">
@Body
</article>
</main>
</div>

View file

@ -0,0 +1,16 @@
.page {
position: relative;
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
}
.content {
padding: 0;
height: 100%;
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
namespace MemoryGame.Models;
public sealed class GameSession
{
public required List<Round> Rounds { get; init; }
public int CurrentRoundIndex { get; set; }
public DateTimeOffset StartTime { get; init; }
public DateTimeOffset? EndTime { get; set; }
public Round? CurrentRound => CurrentRoundIndex < Rounds.Count ? Rounds[CurrentRoundIndex] : null;
public bool IsComplete => CurrentRoundIndex >= Rounds.Count;
public int CorrectAnswers => Rounds.Count(r => r.SelectedAnswer != null && r.IsCorrect);
public int IncorrectAnswers => Rounds.Count(r => r.SelectedAnswer != null && !r.IsCorrect);
}

View file

@ -0,0 +1,6 @@
namespace MemoryGame.Models;
public sealed class GameStatistics
{
public required List<GameWord> Words { get; init; }
}

View file

@ -0,0 +1,23 @@
namespace MemoryGame.Models;
public sealed class GameWord
{
public required string Word { get; init; }
public required string ImagePath { get; init; }
public int CorrectCount { get; set; }
public int IncorrectCount { get; set; }
public double Priority
{
get
{
if (CorrectCount == 0 && IncorrectCount == 0)
return 1.0;
var totalAttempts = CorrectCount + IncorrectCount;
var incorrectRatio = (double)IncorrectCount / totalAttempts;
return incorrectRatio * 2 + 0.5;
}
}
}

View file

@ -0,0 +1,10 @@
namespace MemoryGame.Models;
public sealed class Round
{
public required RoundType Type { get; init; }
public required GameWord CorrectAnswer { get; init; }
public required List<GameWord> Options { get; init; }
public GameWord? SelectedAnswer { get; set; }
public bool IsCorrect => SelectedAnswer?.Word == CorrectAnswer.Word;
}

View file

@ -0,0 +1,7 @@
namespace MemoryGame.Models;
public enum RoundType
{
ImageWithWords,
WordWithImages
}

148
MemoryGame/Pages/Home.razor Normal file
View file

@ -0,0 +1,148 @@
@page "/"
@using MemoryGame.Models
@using MemoryGame.Services
@inject GameService GameService
@inject NavigationManager Navigation
<PageTitle>Memory-Spiel</PageTitle>
<div class="memory-game-container">
@if (!_initialized)
{
<div class="loading">
<h2>Lädt...</h2>
</div>
}
else if (_currentSession == null)
{
<div class="start-screen">
<h1>Memory-Spiel</h1>
<p class="subtitle">Übe das Zuordnen von Bildern zu Wörtern</p>
<button class="btn-start" @onclick="StartNewGame">Neues Spiel starten</button>
</div>
}
else if (_currentSession.IsComplete)
{
<div class="results-screen">
<h1>Spiel beendet!</h1>
<div class="results">
<div class="result-item correct">
<span class="result-label">Richtig:</span>
<span class="result-value">@_currentSession.CorrectAnswers</span>
</div>
<div class="result-item incorrect">
<span class="result-label">Falsch:</span>
<span class="result-value">@_currentSession.IncorrectAnswers</span>
</div>
</div>
<button class="btn-start" @onclick="StartNewGame">Nochmal spielen</button>
</div>
}
else
{
var round = _currentSession.CurrentRound;
if (round != null)
{
<div class="game-screen">
<div class="game-header">
<div class="round-counter">
Runde @(_currentSession.CurrentRoundIndex + 1) von @_currentSession.Rounds.Count
</div>
</div>
@if (round.Type == RoundType.ImageWithWords)
{
<div class="question-area">
<img src="@round.CorrectAnswer.ImagePath" alt="Question image" class="question-image" />
</div>
<div class="options-area">
@foreach (var option in round.Options)
{
<button class="option-button word-option" @onclick="() => SelectAnswer(option)">
@option.Word
</button>
}
</div>
}
else
{
<div class="question-area">
<div class="question-word">@round.CorrectAnswer.Word</div>
</div>
<div class="options-area image-options">
@foreach (var option in round.Options)
{
<button class="option-button image-option" @onclick="() => SelectAnswer(option)">
<img src="@option.ImagePath" alt="@option.Word" />
</button>
}
</div>
}
@if (_showFeedback && round.SelectedAnswer != null)
{
<div class="feedback @(_feedbackClass)">
@_feedbackMessage
</div>
}
</div>
}
}
</div>
@code {
private bool _initialized;
private GameSession? _currentSession;
private bool _showFeedback;
private string _feedbackMessage = "";
private string _feedbackClass = "";
protected override async Task OnInitializedAsync()
{
await GameService.InitializeAsync();
_initialized = true;
}
private void StartNewGame()
{
_currentSession = GameService.CreateNewGame();
_showFeedback = false;
}
private async Task SelectAnswer(GameWord selectedWord)
{
if (_currentSession?.CurrentRound == null || _showFeedback)
return;
var round = _currentSession.CurrentRound;
round.SelectedAnswer = selectedWord;
_showFeedback = true;
if (round.IsCorrect)
{
_feedbackMessage = "Richtig! ✓";
_feedbackClass = "correct";
}
else
{
_feedbackMessage = $"Falsch. Die richtige Antwort ist: {round.CorrectAnswer.Word}";
_feedbackClass = "incorrect";
}
await GameService.UpdateStatisticsAsync(round);
StateHasChanged();
await Task.Delay(2000);
_showFeedback = false;
_currentSession.CurrentRoundIndex++;
if (_currentSession.IsComplete)
{
_currentSession.EndTime = DateTimeOffset.UtcNow;
}
StateHasChanged();
}
}

View file

@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

14
MemoryGame/Program.cs Normal file
View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MemoryGame;
using MemoryGame.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<LocalStorageService>();
builder.Services.AddScoped<GameService>();
await builder.Build().RunAsync();

View file

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5255",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,196 @@
using MemoryGame.Models;
namespace MemoryGame.Services;
public sealed class GameService
{
private readonly LocalStorageService _localStorage;
private readonly HttpClient _httpClient;
private readonly Random _random = new();
private GameStatistics? _statistics;
private List<GameWord>? _availableWords;
public GameService(LocalStorageService localStorage, HttpClient httpClient)
{
_localStorage = localStorage;
_httpClient = httpClient;
}
public async Task InitializeAsync()
{
await LoadImagesFromFolderAsync();
_statistics = await _localStorage.LoadStatisticsAsync();
if (_statistics == null)
{
_statistics = new GameStatistics { Words = new List<GameWord>(_availableWords ?? []) };
}
else
{
if (_availableWords != null)
{
foreach (var availableWord in _availableWords)
{
var existingWord = _statistics.Words.FirstOrDefault(w => w.Word == availableWord.Word);
if (existingWord == null)
{
_statistics.Words.Add(availableWord);
}
}
}
}
}
private async Task LoadImagesFromFolderAsync()
{
_availableWords = [];
var commonWords = new[]
{
"ball", "bed", "book", "car", "cat", "chair", "clock", "cup",
"dog", "door", "fork", "hat", "house", "knife", "phone",
"shoe", "spoon", "table", "tree", "window"
};
var loadTasks = commonWords.Select(async word =>
{
var imagePath = $"images/{word}.svg";
try
{
var response = await _httpClient.GetAsync(imagePath, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var germanWord = ConvertToGermanWord(word);
return new GameWord
{
Word = germanWord,
ImagePath = imagePath,
CorrectCount = 0,
IncorrectCount = 0
};
}
}
catch
{
}
return null;
});
var results = await Task.WhenAll(loadTasks);
_availableWords = results.Where(w => w != null).Cast<GameWord>().ToList();
}
private static string ConvertToGermanWord(string englishWord)
{
return englishWord.ToLower() switch
{
"fork" => "Gabel",
"knife" => "Messer",
"spoon" => "Löffel",
"door" => "Tür",
"window" => "Fenster",
"ball" => "Ball",
"house" => "Haus",
"tree" => "Baum",
"car" => "Auto",
"cat" => "Katze",
"dog" => "Hund",
"chair" => "Stuhl",
"table" => "Tisch",
"cup" => "Tasse",
"book" => "Buch",
"clock" => "Uhr",
"bed" => "Bett",
"phone" => "Telefon",
"shoe" => "Schuh",
"hat" => "Hut",
_ => char.ToUpper(englishWord[0]) + englishWord[1..].ToLower()
};
}
public GameSession CreateNewGame()
{
var rounds = new List<Round>();
for (var i = 0; i < 10; i++)
{
var roundType = _random.Next(2) == 0 ? RoundType.ImageWithWords : RoundType.WordWithImages;
var round = CreateRound(roundType);
rounds.Add(round);
}
return new GameSession
{
Rounds = rounds,
CurrentRoundIndex = 0,
StartTime = DateTimeOffset.UtcNow
};
}
private Round CreateRound(RoundType type)
{
var correctAnswer = SelectWeightedRandomWord();
var incorrectAnswers = SelectRandomWords(3, correctAnswer);
var options = new List<GameWord> { correctAnswer };
options.AddRange(incorrectAnswers);
options = [.. options.OrderBy(_ => _random.Next())];
return new Round
{
Type = type,
CorrectAnswer = correctAnswer,
Options = options
};
}
private GameWord SelectWeightedRandomWord()
{
if (_statistics == null || _statistics.Words.Count == 0)
throw new InvalidOperationException("No words available");
var totalPriority = _statistics.Words.Sum(w => w.Priority);
var randomValue = _random.NextDouble() * totalPriority;
var cumulativePriority = 0.0;
foreach (var word in _statistics.Words)
{
cumulativePriority += word.Priority;
if (randomValue <= cumulativePriority)
return word;
}
return _statistics.Words[^1];
}
private List<GameWord> SelectRandomWords(int count, GameWord exclude)
{
if (_statistics == null)
throw new InvalidOperationException("Statistics not initialized");
var availableWords = _statistics.Words.Where(w => w.Word != exclude.Word).ToList();
return [.. availableWords.OrderBy(_ => _random.Next()).Take(count)];
}
public async Task UpdateStatisticsAsync(Round round)
{
if (_statistics == null || round.SelectedAnswer == null)
return;
var word = _statistics.Words.FirstOrDefault(w => w.Word == round.CorrectAnswer.Word);
if (word == null)
return;
if (round.IsCorrect)
{
word.CorrectCount++;
}
else
{
word.IncorrectCount++;
}
await _localStorage.SaveStatisticsAsync(_statistics);
}
}

View file

@ -0,0 +1,38 @@
using System.Text.Json;
using Microsoft.JSInterop;
using MemoryGame.Models;
namespace MemoryGame.Services;
public sealed class LocalStorageService
{
private readonly IJSRuntime _jsRuntime;
private const string StatisticsKey = "memoryGameStatistics";
public LocalStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<GameStatistics?> LoadStatisticsAsync()
{
try
{
var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", StatisticsKey);
if (string.IsNullOrEmpty(json))
return null;
return JsonSerializer.Deserialize<GameStatistics>(json);
}
catch
{
return null;
}
}
public async Task SaveStatisticsAsync(GameStatistics statistics)
{
var json = JsonSerializer.Serialize(statistics);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", StatisticsKey, json);
}
}

10
MemoryGame/_Imports.razor Normal file
View file

@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using MemoryGame
@using MemoryGame.Layout

View file

@ -0,0 +1,345 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: absolute;
display: block;
width: 8rem;
height: 8rem;
inset: 20vh 0 auto 0;
margin: 0 auto 0 auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
.memory-game-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.start-screen, .results-screen {
text-align: center;
padding: 40px;
}
.start-screen h1, .results-screen h1 {
font-size: 3rem;
margin-bottom: 20px;
color: #333;
}
.subtitle {
font-size: 1.5rem;
color: #666;
margin-bottom: 40px;
}
.btn-start {
font-size: 1.8rem;
padding: 20px 60px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s;
font-weight: bold;
}
.btn-start:hover {
background-color: #45a049;
}
.btn-start:active {
transform: scale(0.98);
}
.game-screen {
width: 100%;
max-width: 900px;
}
.game-header {
text-align: center;
margin-bottom: 30px;
}
.round-counter {
font-size: 1.5rem;
font-weight: bold;
color: #333;
padding: 10px;
background-color: #f0f0f0;
border-radius: 8px;
}
.question-area {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30px;
min-height: 300px;
}
.question-image {
max-width: 100%;
max-height: 400px;
object-fit: contain;
border: 3px solid #ddd;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.question-word {
font-size: 4rem;
font-weight: bold;
color: #333;
text-transform: capitalize;
}
.options-area {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.options-area.image-options {
grid-template-columns: 1fr 1fr;
}
.option-button {
padding: 30px;
font-size: 2rem;
background-color: #f8f9fa;
border: 3px solid #ddd;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
text-transform: capitalize;
}
.option-button:hover {
background-color: #e9ecef;
border-color: #4CAF50;
transform: scale(1.05);
}
.option-button:active {
transform: scale(0.98);
}
.option-button.image-option {
padding: 20px;
height: 250px;
display: flex;
align-items: center;
justify-content: center;
}
.option-button.image-option img {
max-width: 100%;
max-height: 200px;
object-fit: contain;
}
.feedback {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 40px 60px;
font-size: 2.5rem;
font-weight: bold;
border-radius: 15px;
z-index: 1000;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
animation: fadeIn 0.3s;
}
.feedback.correct {
background-color: #4CAF50;
color: white;
}
.feedback.incorrect {
background-color: #f44336;
color: white;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.results {
display: flex;
justify-content: center;
gap: 40px;
margin: 40px 0;
flex-wrap: wrap;
}
.result-item {
padding: 30px;
border-radius: 10px;
min-width: 200px;
text-align: center;
}
.result-item.correct {
background-color: #4CAF50;
color: white;
}
.result-item.incorrect {
background-color: #f44336;
color: white;
}
.result-label {
display: block;
font-size: 1.5rem;
margin-bottom: 10px;
}
.result-value {
display: block;
font-size: 3.5rem;
font-weight: bold;
}
.loading {
text-align: center;
padding: 40px;
}
@media (max-width: 768px) {
.question-word {
font-size: 3rem;
}
.option-button {
font-size: 1.5rem;
padding: 20px;
}
.btn-start {
font-size: 1.5rem;
padding: 15px 40px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<circle cx="0" cy="0" r="50" fill="#FF6347" stroke="#333" stroke-width="2"/>
<path d="M -35,-35 Q 0,-50 35,-35" fill="none" stroke="#333" stroke-width="2"/>
<path d="M -35,35 Q 0,50 35,35" fill="none" stroke="#333" stroke-width="2"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">BALL</text>
</svg>

After

Width:  |  Height:  |  Size: 494 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-50" y="-10" width="100" height="20" fill="#8B4513"/>
<rect x="-50" y="-30" width="100" height="20" fill="#4169E1"/>
<line x1="-45" y1="10" x2="-45" y2="30" stroke="#8B4513" stroke-width="5"/>
<line x1="45" y1="10" x2="45" y2="30" stroke="#8B4513" stroke-width="5"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">BETT</text>
</svg>

After

Width:  |  Height:  |  Size: 540 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-35" y="-40" width="70" height="80" fill="#8B0000" stroke="#333" stroke-width="2"/>
<line x1="0" y1="-40" x2="0" y2="40" stroke="#333" stroke-width="2"/>
<rect x="-30" y="-30" width="25" height="10" fill="#FFF"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">BUCH</text>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-50" y="-10" width="100" height="30" fill="#FF0000" stroke="#333" stroke-width="2"/>
<rect x="-35" y="-30" width="40" height="20" fill="#FF0000" stroke="#333" stroke-width="2"/>
<circle cx="-30" cy="20" r="10" fill="#333"/>
<circle cx="30" cy="20" r="10" fill="#333"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">AUTO</text>
</svg>

After

Width:  |  Height:  |  Size: 542 B

View file

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<ellipse cx="0" cy="0" rx="40" ry="30" fill="#FFA500"/>
<circle cx="0" cy="-25" r="25" fill="#FFA500"/>
<polygon points="-20,-45 -25,-55 -15,-40" fill="#FFA500"/>
<polygon points="20,-45 25,-55 15,-40" fill="#FFA500"/>
<circle cx="-10" cy="-25" r="3" fill="#333"/>
<circle cx="10" cy="-25" r="3" fill="#333"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">KATZE</text>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-30" y="-40" width="60" height="10" fill="#8B4513"/>
<line x1="-25" y1="-30" x2="-25" y2="40" stroke="#8B4513" stroke-width="6"/>
<line x1="25" y1="-30" x2="25" y2="40" stroke="#8B4513" stroke-width="6"/>
<line x1="-25" y1="-40" x2="-25" y2="-60" stroke="#8B4513" stroke-width="6"/>
<line x1="25" y1="-40" x2="25" y2="-60" stroke="#8B4513" stroke-width="6"/>
<line x1="-25" y1="-60" x2="25" y2="-60" stroke="#8B4513" stroke-width="6"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">STUHL</text>
</svg>

After

Width:  |  Height:  |  Size: 718 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<circle cx="0" cy="0" r="50" fill="#FFF" stroke="#333" stroke-width="3"/>
<line x1="0" y1="0" x2="0" y2="-30" stroke="#333" stroke-width="3"/>
<line x1="0" y1="0" x2="20" y2="0" stroke="#333" stroke-width="2"/>
<circle cx="0" cy="0" r="3" fill="#333"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">UHR</text>
</svg>

After

Width:  |  Height:  |  Size: 516 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<path d="M -25,-30 L -20,30 L 20,30 L 25,-30 Z" fill="#87CEEB" stroke="#333" stroke-width="2"/>
<path d="M 25,-10 Q 40,-10 40,10 Q 40,30 25,30" fill="none" stroke="#333" stroke-width="2"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">TASSE</text>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View file

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<ellipse cx="0" cy="0" rx="45" ry="30" fill="#8B4513"/>
<circle cx="-15" cy="-20" r="20" fill="#8B4513"/>
<ellipse cx="-30" cy="-30" rx="10" ry="20" fill="#8B4513"/>
<ellipse cx="0" cy="-30" rx="10" ry="20" fill="#8B4513"/>
<circle cx="-20" cy="-25" r="3" fill="#333"/>
<circle cx="-10" cy="-25" r="3" fill="#333"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">HUND</text>
</svg>

After

Width:  |  Height:  |  Size: 588 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-35" y="-60" width="70" height="120" fill="#8B4513" stroke="#654321" stroke-width="3"/>
<circle cx="20" cy="0" r="4" fill="#FFD700"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">TÜR</text>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<line x1="0" y1="-60" x2="0" y2="60" stroke="#666" stroke-width="6"/>
<line x1="-20" y1="-60" x2="-20" y2="0" stroke="#666" stroke-width="4"/>
<line x1="-10" y1="-60" x2="-10" y2="0" stroke="#666" stroke-width="4"/>
<line x1="10" y1="-60" x2="10" y2="0" stroke="#666" stroke-width="4"/>
<line x1="20" y1="-60" x2="20" y2="0" stroke="#666" stroke-width="4"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">GABEL</text>
</svg>

After

Width:  |  Height:  |  Size: 627 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<ellipse cx="0" cy="10" rx="50" ry="10" fill="#8B4513" stroke="#333" stroke-width="2"/>
<path d="M -30,-30 Q -30,-50 0,-50 Q 30,-50 30,-30 L 20,10 L -20,10 Z" fill="#8B4513" stroke="#333" stroke-width="2"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">HUT</text>
</svg>

After

Width:  |  Height:  |  Size: 462 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<polygon points="0,-50 50,0 -50,0" fill="#8B4513"/>
<rect x="-40" y="0" width="80" height="60" fill="#DEB887" stroke="#333" stroke-width="2"/>
<rect x="-15" y="20" width="30" height="40" fill="#654321"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">HAUS</text>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100) rotate(-45)">
<rect x="-10" y="-50" width="20" height="100" fill="#666"/>
<polygon points="-10,-60 0,-80 10,-60" fill="#999"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">MESSER</text>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-25" y="-50" width="50" height="100" rx="5" fill="#333" stroke="#666" stroke-width="2"/>
<rect x="-20" y="-40" width="40" height="70" fill="#87CEEB"/>
<circle cx="0" cy="40" r="5" fill="#666"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">TELEFON</text>
</svg>

After

Width:  |  Height:  |  Size: 466 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<path d="M -40,20 L -30,-10 L 30,-10 L 40,20 Z" fill="#8B4513" stroke="#333" stroke-width="2"/>
<ellipse cx="0" cy="20" rx="40" ry="15" fill="#654321" stroke="#333" stroke-width="2"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">SCHUH</text>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<ellipse cx="0" cy="-40" rx="20" ry="30" fill="#666"/>
<rect x="-5" y="-10" width="10" height="70" fill="#666"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">LÖFFEL</text>
</svg>

After

Width:  |  Height:  |  Size: 372 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-50" y="-10" width="100" height="10" fill="#8B4513"/>
<line x1="-40" y1="0" x2="-40" y2="40" stroke="#8B4513" stroke-width="6"/>
<line x1="40" y1="0" x2="40" y2="40" stroke="#8B4513" stroke-width="6"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">TISCH</text>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-10" y="20" width="20" height="40" fill="#8B4513"/>
<circle cx="0" cy="-20" r="40" fill="#228B22"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">BAUM</text>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<g transform="translate(100,100)">
<rect x="-40" y="-40" width="80" height="80" fill="#87CEEB" stroke="#333" stroke-width="3"/>
<line x1="-40" y1="0" x2="40" y2="0" stroke="#333" stroke-width="3"/>
<line x1="0" y1="-40" x2="0" y2="40" stroke="#333" stroke-width="3"/>
</g>
<text x="100" y="180" text-anchor="middle" font-size="20" fill="#666">FENSTER</text>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MemoryGame</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="MemoryGame.styles.css" rel="stylesheet" />
<script type="importmap"></script>
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

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

View file

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

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

View file

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

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

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

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

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

173
README.md Normal file
View file

@ -0,0 +1,173 @@
# Memory Game
A Blazor WebAssembly application designed to help people with dementia practice image-to-word associations. The game presents simple everyday objects in German and helps users practice matching images with words through adaptive learning.
![Memory Game Start Screen](https://github.com/user-attachments/assets/242b1ae5-a947-4c98-b0b9-6c2d4250911d)
## Features
- **10-Round Game Sessions**: Each game consists of 10 rounds to provide a focused practice session
- **Dual Game Modes**:
- Show an image with 4 word choices
- Show a word with 4 image choices
- **Adaptive Learning**:
- Words answered correctly become less frequent in future games
- Words answered incorrectly appear more often for additional practice
- **Progress Tracking**: Statistics are stored in browser local storage and persist across sessions
- **Tablet-Optimized UI**: Designed for 11" tablets with large, easy-to-tap buttons
- **Simple Vocabulary**: Uses everyday objects (Gabel, Messer, Tür, Ball, Haus, etc.)
- **Immediate Feedback**: Shows whether the answer was correct or incorrect after each round
- **Game Results**: Displays total correct and incorrect answers at the end of each game
- **German Language**: All user-facing text is in German for German-speaking users
## Screenshots
### Start Screen
The game presents a clean, simple interface optimized for people with dementia.
![Start Screen](https://github.com/user-attachments/assets/242b1ae5-a947-4c98-b0b9-6c2d4250911d)
### Image with Word Choices
In this mode, players see an image and must select the correct German word from 4 options.
![Image with Words Mode](https://github.com/user-attachments/assets/1fbe34db-3376-4cf9-ac76-74ee6c6ece82)
### Word with Image Choices
Alternatively, players see a German word and must select the matching image from 4 options.
![Word with Images Mode](https://github.com/user-attachments/assets/754cc680-16ff-4360-b961-9ad1259aea22)
### Game Results
After completing 10 rounds, players see their results with correct and incorrect answer counts.
![Results Screen](https://github.com/user-attachments/assets/2d73bbe7-502c-47ce-a61a-2c4cbfa74572)
### German Translation
All text visible to users is in German, making the game accessible for German-speaking users.
![German Translation Demo](https://github.com/user-attachments/assets/64975c32-317b-4789-9ee6-07234e6c1084)
## Technology Stack
- **.NET 10.0**: Using the latest .NET framework
- **Blazor WebAssembly**: Client-side web application that runs entirely in the browser
- **Local Storage**: Browser-based persistence for tracking progress across sessions
- **SVG Images**: Scalable vector graphics for clear display on all screen sizes
## Getting Started
### Prerequisites
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
### Running the Application
1. Clone the repository:
```bash
git clone https://github.com/clayschaad/MemoryGame.git
cd MemoryGame
```
2. Navigate to the project directory:
```bash
cd MemoryGame
```
3. Run the application:
```bash
dotnet run
```
4. Open your browser and navigate to the URL shown in the console (typically `http://localhost:5000`)
### Building for Production
To build the application for deployment:
```bash
dotnet publish -c Release
```
The published files will be in `bin/Release/net10.0/publish/wwwroot/` and can be hosted on any static web server.
## How to Use
1. **Start Screen**: Click "Start New Game" to begin a new game session
2. **Game Rounds**:
- Read the question (image or word)
- Select the correct answer from 4 options
- Receive immediate feedback
- Wait 2 seconds before automatically moving to the next round
3. **Results**: After 10 rounds, view your score and click "Play Again" to start a new game
## Game Statistics
The application tracks performance for each word/image pair:
- **Correct Count**: Number of times the word was answered correctly
- **Incorrect Count**: Number of times the word was answered incorrectly
- **Priority**: Calculated based on performance (higher priority = more frequent appearance)
This data is stored in browser local storage and persists across sessions, enabling the adaptive learning feature.
## Project Structure
```
MemoryGame/
├── Models/ # Data models for game state
│ ├── GameWord.cs
│ ├── GameSession.cs
│ ├── GameStatistics.cs
│ ├── Round.cs
│ └── RoundType.cs
├── Services/ # Business logic and storage
│ ├── GameService.cs
│ └── LocalStorageService.cs
├── Pages/ # Blazor pages
│ └── Home.razor
├── Layout/ # Layout components
│ └── MainLayout.razor
└── wwwroot/
├── images/ # SVG image files
└── css/ # Stylesheets
```
## Adding New Words/Images
The game automatically discovers images from the `wwwroot/images/` folder. To add new words and images:
1. **Add your image**: Place an SVG image in `wwwroot/images/` with the English filename (e.g., `apple.svg`, `orange.svg`)
2. **Add to discovery list**: Add the filename (without extension) to the `commonWords` array in `Services/GameService.cs`:
```csharp
var commonWords = new[]
{
"ball", "bed", "book", "car", "cat", "chair", "clock", "cup",
"dog", "door", "fork", "hat", "house", "knife", "phone",
"shoe", "spoon", "table", "tree", "window",
"apple", "orange" // Add new words here
};
```
3. **Add German translation** (optional): If you want a custom German translation, update the `ConvertToGermanWord` method in `Services/GameService.cs`:
```csharp
"apple" => "Apfel",
"orange" => "Orange",
```
If no translation is provided, the filename will be capitalized and used as-is.
The game will automatically discover and load images that exist in the folder. No manifest file needed!
## Browser Compatibility
The application works in all modern browsers that support WebAssembly:
- Chrome/Edge (recommended)
- Firefox
- Safari
- Opera
## License
This project is open source and available under the MIT License.
## Acknowledgments
Built with Blazor WebAssembly for optimal performance and offline capability.