feat: Move hints to backend, clean up game service on frontend

This commit is contained in:
2025-03-30 13:15:05 +00:00
parent f5a131ec4e
commit b646bde79a
6 changed files with 155 additions and 178 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using backend.Models; using backend.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using System.Security.Claims; using System.Security.Claims;
using DotNetEnv;
namespace backend.Controllers; namespace backend.Controllers;
@@ -66,7 +67,7 @@ public class GamesController(GameContext context) : ControllerBase {
} }
var newGame = new Game { var newGame = new Game {
UserId = (long)user, UserId = user.Value,
Deck = [.. Deck = [..
from shape in Enum.GetValues<CardShape>().Cast<CardShape>() from shape in Enum.GetValues<CardShape>().Cast<CardShape>()
from color in Enum.GetValues<CardColor>().Cast<CardColor>() from color in Enum.GetValues<CardColor>().Cast<CardColor>()
@@ -148,8 +149,9 @@ public class GamesController(GameContext context) : ControllerBase {
[Route("[action]/{id}")] [Route("[action]/{id}")]
[Authorize] [Authorize]
public async Task<ActionResult<List<int[]>>> SetsInHand(long id) { public async Task<ActionResult<List<int[]>>> SetsInHand(long id) {
// Only show if user is admin (id < 1)
var user = ParseUserId(); var user = ParseUserId();
if (user == null) { if (user == null || user > 0) {
return BadRequest(); return BadRequest();
} }
@@ -169,4 +171,34 @@ public class GamesController(GameContext context) : ControllerBase {
return res; return res;
} }
[HttpGet]
[Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<List<ushort>>> Hint(long id) {
var user = ParseUserId();
if (user == null) {
return BadRequest();
}
var game = await _context.Games
.Where(g => g.Id == id && g.UserId == user)
.FirstOrDefaultAsync();
if (game == null) {
return NotFound();
}
var sets = game.GetIndicesOfSet();
if (sets == null || sets.Count == 0) {
return BadRequest("No sets found in hand.");
}
game.Hints++;
await _context.SaveChangesAsync();
return sets[0].Take(2).Select(i => (ushort)i).ToList();
}
} }

View File

@@ -61,7 +61,7 @@ namespace backend.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.ToTable("Games"); b.ToTable("Games", (string)null);
}); });
modelBuilder.Entity("backend.Models.User", b => modelBuilder.Entity("backend.Models.User", b =>
@@ -94,7 +94,7 @@ namespace backend.Migrations
b.HasIndex("Username") b.HasIndex("Username")
.IsUnique(); .IsUnique();
b.ToTable("Users"); b.ToTable("Users", (string)null);
}); });
modelBuilder.Entity("backend.Models.Game", b => modelBuilder.Entity("backend.Models.Game", b =>

View File

@@ -5,7 +5,6 @@ public struct SetCheckResult {
public bool? IsFinished { get; set; } public bool? IsFinished { get; set; }
public Game NewState { get; set; } public Game NewState { get; set; }
} }
// todo: add check to routes if owner is user
public class Game { public class Game {
public long Id { get; set; } public long Id { get; set; }
@@ -21,8 +20,8 @@ public class Game {
public void ShuffleDeck() { public void ShuffleDeck() {
if (Deck == null) return; if (Deck == null) return;
Random rand = new Random(); Random rand = new();
Deck = Deck.OrderBy(_ => rand.Next()).ToArray(); Deck = [.. Deck.OrderBy(_ => rand.Next())];
} }
public void DealHand(int max = 12) { public void DealHand(int max = 12) {
@@ -113,17 +112,14 @@ public class Game {
foreach (var index in indices.OrderByDescending(i => i)) { foreach (var index in indices.OrderByDescending(i => i)) {
var foundList = Found.ToList(); var foundList = Found.ToList();
foundList.Add(Hand[index]); foundList.Add(Hand[index]);
Found = foundList.ToArray(); Found = [.. foundList];
if (Hand.Length < 13) if (Hand.Length < 13) {
{
ReplaceCardInHand(index); ReplaceCardInHand(index);
} } else {
else
{
var handList = Hand.ToList(); var handList = Hand.ToList();
handList.RemoveAt(index); handList.RemoveAt(index);
Hand = handList.ToArray(); Hand = [.. handList];
} }
} }
@@ -142,8 +138,8 @@ public class Game {
} }
public List<int[]> GetIndicesOfSet() { public List<int[]> GetIndicesOfSet() {
if (Hand == null) return new List<int[]>(); if (Hand == null) return [];
List<int[]> res = new List<int[]>(); List<int[]> res = [];
for (int i = 0; i < Hand.Length; i++) { for (int i = 0; i < Hand.Length; i++) {
for (int j = i + 1; j < Hand.Length; j++) { for (int j = i + 1; j < Hand.Length; j++) {
@@ -151,7 +147,6 @@ public class Game {
var result = SetResult([(ushort)i, (ushort)j, (ushort)k]); var result = SetResult([(ushort)i, (ushort)j, (ushort)k]);
if (result == true) { if (result == true) {
Console.WriteLine($"Found set at {i}, {j}, {k}");
res.Add([i, j, k]); res.Add([i, j, k]);
} }
} }

View File

@@ -13,11 +13,11 @@
<div class="flex justify-between"> <div class="flex justify-between">
<span class="font-bold">Incorrect Matches</span> <span class="font-bold">Incorrect Matches</span>
<span>{{ gameService.stats().fails }}</span> <span>{{ gameService.fails }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="font-bold">Hints Requested</span> <span class="font-bold">Hints Requested</span>
<span>{{ gameService.stats().hints }}</span> <span>{{ gameService.hints }}</span>
</div> </div>
</div> </div>
@@ -67,19 +67,19 @@
<p> <p>
<span class="font-bold">Deck Size</span> <span class="font-bold">Deck Size</span>
<span class="float-right">{{ gameService.stats().size }}</span> <span class="float-right">{{ gameService.deck.length }}</span>
</p> </p>
<p> <p>
<span class="font-bold">Incorrect Matches</span> <span class="font-bold">Incorrect Matches</span>
<span class="float-right">{{ gameService.stats().fails }}</span> <span class="float-right">{{ gameService.fails }}</span>
</p> </p>
<p> <p>
<span class="font-bold">Hints Requested</span> <span class="font-bold">Hints Requested</span>
<span class="float-right">{{ gameService.stats().hints }}</span> <span class="float-right">{{ gameService.hints }}</span>
</p> </p>
<p> <p>
<span class="font-bold" style="padding-right: 100px;">Started on</span> <span class="font-bold" style="padding-right: 100px;">Started on</span>
<span class="float-right">{{ gameService.stats().dateStarted | date:'dd-MM-YYYY HH:mm' }}</span> <span class="float-right">{{ gameService.startDate | date:'dd-MM-YYYY HH:mm' }}</span>
</p> </p>
<div class="mt-2"> <div class="mt-2">

View File

@@ -13,7 +13,7 @@ import { ActivatedRoute, UrlSegment, Router } from '@angular/router';
}) })
export class GameComponent { export class GameComponent {
gameId: string = ''; gameId: number = -1;
hint: Card[] = [] hint: Card[] = []
gameService: GameService = inject(GameService); gameService: GameService = inject(GameService);
route: ActivatedRoute = inject(ActivatedRoute); route: ActivatedRoute = inject(ActivatedRoute);
@@ -21,7 +21,7 @@ export class GameComponent {
constructor() { constructor() {
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
this.gameId = params['id']; this.gameId = parseInt(params['id']);
this.attachId(this.gameId); this.attachId(this.gameId);
}); });
@@ -31,10 +31,10 @@ export class GameComponent {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
async attachId(i: string) { async attachId(i: number) {
const id = await this.gameService.initGame(i); const id = await this.gameService.initGame(i);
this.route.snapshot.url.push(new UrlSegment(id, {})); this.route.snapshot.url.push(new UrlSegment(id.toString(), {}));
window.history.replaceState({}, '', `/game/${id}`); window.history.replaceState({}, '', `/game/${id}`);
} }
@@ -49,8 +49,8 @@ export class GameComponent {
} }
async showHint() { async showHint() {
if (this.gameService.possibleSets.length < 0) return console.error('No valid sets found'); // if (this.hint.length > 0) return;
this.hint = [this.gameService.possibleSets[0][0], this.gameService.possibleSets[0][1]] this.hint = await this.gameService.showHint();
} }
} }

View File

@@ -1,179 +1,129 @@
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Card, toCard } from '../../app/models/card'; import { Card, toCard } from '../../app/models/card';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { AuthService } from '../auth/auth.service';
interface GameResponse {
deck: number[];
hand: number[];
found: number[];
startedAt: Date;
finishedAt?: Date;
fails: number;
hints: number;
id: number;
}
interface SetCheckResponse {
isSet: boolean;
newState: GameResponse;
}
type HintResponse = number[];
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class GameService { export class GameService {
private http = inject(HttpClient);
private API_URL = 'http://localhost:5224/api/v1/Games'; private API_URL = 'http://localhost:5224/api/v1/Games';
public deck: Card[] = []; public deck: Card[] = [];
public hand: Card[] = []; public hand: Card[] = [];
public selectedCards: Card[] = [];
public foundSets: Card[][] = []; public foundSets: Card[][] = [];
public possibleSets: Card[][] = []; public possibleSets: Card[][] = [];
public fails: number = 0; public fails: number = 0;
public hints: number = 0; public hints: number = 0;
public gameId: number = 0; public gameId: number = 0;
public startDate: Date = new Date(); public startDate: Date = new Date();
public finishedAt?: Date; public finishedAt?: Date;
public selectedCards: Card[] = [];
constructor( constructor() {}
private http: HttpClient,
) {}
public initGame(gameId?: string): Promise<string> { public updateState(game: GameResponse): void {
this.deck = []; this.gameId = game.id;
this.hand = []; this.fails = game.fails;
return gameId ? this.initializeExistingGame(gameId) : this.initializeDeck(); this.hints = game.hints;
} this.startDate = game.startedAt;
this.finishedAt = game.finishedAt;
public stats(): any {
const formattedDate = `${this.startDate.getDate().toString().padStart(2, '0')}-${(this.startDate.getMonth() + 1).toString().padStart(2, '0')}-${this.startDate.getFullYear()} ${this.startDate.getHours().toString().padStart(2, '0')}:${this.startDate.getMinutes().toString().padStart(2, '0')}`;
return { size: this.deck.length, fails: this.fails, hints: this.hints, dateStarted: formattedDate };
}
public async initializeExistingGame(gameId: string): Promise<string> {
try {
const response = await lastValueFrom(this.http.get<any>(`${this.API_URL}/${gameId}`));
response.deck.forEach((card: number) => {
this.deck.push(toCard(card));
});
response.hand.forEach((card: number) => {
this.hand.push(toCard(card));
});
this.startDate = new Date(response.startedAt);
this.fails = response.fails;
this.hints = response.hints;
this.finishedAt = response.finishedAt ? new Date(response.finishedAt) : undefined;
this.deck = game.deck.map(toCard);
this.hand = game.hand.map(toCard);
this.foundSets = []; this.foundSets = [];
for (let i = 0; i < response.found.length; i += 3) {
this.foundSets.push(response.found.slice(i, i + 3).map((card: number) => toCard(card))); // Is stored as [card1, card2, card3, ...], chunk into sets of 3
game.found.reduce((acc: Card[][], card: number, index: number) => {
if (index % 3 === 0) acc.push([]);
acc[acc.length - 1].push(toCard(card));
return acc;
}, this.foundSets);
// Shows all sets in hand for admins
this.getSetsInHand();
} }
this.gameId = response.id; public async initGame(gameId?: number): Promise<number> {
this.updateSets(); // Fetch game data from the server, use lastValueFrom to convert Observable to Promise
return response.id.toString(); let game: GameResponse;
} catch (error) { if (gameId) {
console.error('Error initializing existing game', error); game = await lastValueFrom(this.http.get<GameResponse>(`${this.API_URL}/${gameId}`))
throw error; } else {
} game = await lastValueFrom(this.http.post<GameResponse>(this.API_URL, {}));
} }
private async initializeDeck(): Promise<string> { // Update the game state with the fetched data
try { this.updateState(game);
const response = await lastValueFrom(this.http.post<any>(this.API_URL, {}));
response.deck.forEach((card: number) => { return game.id;
this.deck.push(toCard(card));
});
response.hand.forEach((card: number) => {
this.hand.push(toCard(card));
});
this.startDate = new Date(response.startedAt);
this.fails = response.fails;
this.hints = response.hints;
this.gameId = response.id;
this.finishedAt = response.finishedAt ? new Date(response.finishedAt) : undefined;
this.foundSets = [];
for (let i = 0; i < response.found.length; i += 3) {
this.foundSets.push(response.found.slice(i, i + 3).map((card: number) => toCard(card)));
} }
this.updateSets(); public async getSetsInHand(): Promise<void> {
return response.id.toString(); const sets = await lastValueFrom(
} catch (error) {
console.error('Error initializing deck', error);
throw error;
}
}
public async updateSets(): Promise<Card[][]> {
try {
const response = await lastValueFrom(
this.http.get<number[][]>(`${this.API_URL}/SetsInHand/${this.gameId}`) this.http.get<number[][]>(`${this.API_URL}/SetsInHand/${this.gameId}`)
); );
const cards = response.map((set: number[]) => this.possibleSets = sets.map(set => set.map(cardIndex => this.hand[cardIndex]));
set.map((card: number) => this.hand[card])
);
this.possibleSets = cards;
return cards;
} catch (error) {
console.error('Error updating sets', error);
throw error;
}
} }
public async checkSet(cards: number[]): Promise<boolean> { public async checkSet(cards: number[]): Promise<boolean> {
try {
const response = await lastValueFrom( const response = await lastValueFrom(
this.http.post<any>(`${this.API_URL}/CheckSet/${this.gameId}`, cards) this.http.post<SetCheckResponse>(`${this.API_URL}/CheckSet/${this.gameId}`, cards)
); );
this.hand = response.newState.hand.map((card: number) => toCard(card)); this.updateState(response.newState);
this.deck = response.newState.deck.map((card: number) => toCard(card));
this.fails = response.newState.fails;
this.hints = response.newState.hints;
this.finishedAt = response.newState.finishedAt ? new Date(response.newState.finishedAt) : undefined;
this.foundSets = [];
for (let i = 0; i < response.newState.found.length; i += 3) {
this.foundSets.push(response.newState.found.slice(i, i + 3).map((card: number) => toCard(card)));
}
await this.updateSets();
return response.isSet; return response.isSet;
} catch (error) {
console.error('Error checking set', error);
throw error;
}
} }
public selectCard(card: Card): void { public selectCard(card: Card): void {
const cardIndex = this.hand.indexOf(card); // Card not found in hand
if (cardIndex === -1) return; // Card not found in hand if (this.hand.indexOf(card) === -1) return;
// Unselect card if already selected
if (this.selectedCards.includes(card)) { if (this.selectedCards.includes(card)) {
this.selectedCards = this.selectedCards.filter(c => c !== card); this.selectedCards.splice(this.selectedCards.indexOf(card), 1);
// Select card if less than 3 selected
} else if (this.selectedCards.length < 3) { } else if (this.selectedCards.length < 3) {
this.selectedCards.push(card); this.selectedCards.push(card);
} }
// Check if 3 cards are selected. If so, check if they form a set
if (this.selectedCards.length === 3) { if (this.selectedCards.length === 3) {
const indices = this.selectedCards.map(c => this.hand.indexOf(c)); this.checkSet(this.selectedCards.map(c => this.hand.indexOf(c)));
this.checkSet(indices).then(isSet => { this.selectedCards.length = 0;
this.selectedCards = [];
});
} }
} }
public async deleteGame(): Promise<void> { public async showHint(): Promise<Card[]> {
try { const hint = await lastValueFrom(this.http.get<HintResponse>(`${this.API_URL}/Hint/${this.gameId}`));
await lastValueFrom(this.http.delete<void>(`${this.API_URL}/${this.gameId}`));
} catch (error) {
console.error('Error deleting game', error);
throw error;
}
}
public async showHint(): Promise<void> {
try {
await lastValueFrom(this.http.post<void>(`${this.API_URL}/Hint/${this.gameId}`, {}));
this.hints++; this.hints++;
} catch (error) {
console.error('Error showing hint', error); return hint.map((indexOfCard: number) => this.hand[indexOfCard]);
throw error;
} }
public deleteGame(): void {
this.http.delete<void>(`${this.API_URL}/${this.gameId}`).subscribe();
} }
} }