mirror of
https://github.com/Wessel/nhl-setgame.git
synced 2026-06-06 00:05:42 +02:00
feat: Move hints to backend, clean up game service on frontend
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using backend.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
using DotNetEnv;
|
||||
|
||||
namespace backend.Controllers;
|
||||
|
||||
@@ -66,7 +67,7 @@ public class GamesController(GameContext context) : ControllerBase {
|
||||
}
|
||||
|
||||
var newGame = new Game {
|
||||
UserId = (long)user,
|
||||
UserId = user.Value,
|
||||
Deck = [..
|
||||
from shape in Enum.GetValues<CardShape>().Cast<CardShape>()
|
||||
from color in Enum.GetValues<CardColor>().Cast<CardColor>()
|
||||
@@ -148,8 +149,9 @@ public class GamesController(GameContext context) : ControllerBase {
|
||||
[Route("[action]/{id}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<int[]>>> SetsInHand(long id) {
|
||||
// Only show if user is admin (id < 1)
|
||||
var user = ParseUserId();
|
||||
if (user == null) {
|
||||
if (user == null || user > 0) {
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
@@ -169,4 +171,34 @@ public class GamesController(GameContext context) : ControllerBase {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace backend.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Games");
|
||||
b.ToTable("Games", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("backend.Models.User", b =>
|
||||
@@ -94,7 +94,7 @@ namespace backend.Migrations
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("backend.Models.Game", b =>
|
||||
|
||||
@@ -5,7 +5,6 @@ public struct SetCheckResult {
|
||||
public bool? IsFinished { get; set; }
|
||||
public Game NewState { get; set; }
|
||||
}
|
||||
// todo: add check to routes if owner is user
|
||||
|
||||
public class Game {
|
||||
public long Id { get; set; }
|
||||
@@ -21,8 +20,8 @@ public class Game {
|
||||
public void ShuffleDeck() {
|
||||
if (Deck == null) return;
|
||||
|
||||
Random rand = new Random();
|
||||
Deck = Deck.OrderBy(_ => rand.Next()).ToArray();
|
||||
Random rand = new();
|
||||
Deck = [.. Deck.OrderBy(_ => rand.Next())];
|
||||
}
|
||||
|
||||
public void DealHand(int max = 12) {
|
||||
@@ -113,17 +112,14 @@ public class Game {
|
||||
foreach (var index in indices.OrderByDescending(i => i)) {
|
||||
var foundList = Found.ToList();
|
||||
foundList.Add(Hand[index]);
|
||||
Found = foundList.ToArray();
|
||||
Found = [.. foundList];
|
||||
|
||||
if (Hand.Length < 13)
|
||||
{
|
||||
if (Hand.Length < 13) {
|
||||
ReplaceCardInHand(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
var handList = Hand.ToList();
|
||||
handList.RemoveAt(index);
|
||||
Hand = handList.ToArray();
|
||||
Hand = [.. handList];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,20 +138,19 @@ public class Game {
|
||||
}
|
||||
|
||||
public List<int[]> GetIndicesOfSet() {
|
||||
if (Hand == null) return new List<int[]>();
|
||||
List<int[]> res = new List<int[]>();
|
||||
if (Hand == null) return [];
|
||||
List<int[]> res = [];
|
||||
|
||||
for (int i = 0; i < Hand.Length; i++) {
|
||||
for (int j = i + 1; j < Hand.Length; j++) {
|
||||
for (int k = j + 1; k < Hand.Length; k++) {
|
||||
var result = SetResult([(ushort)i, (ushort)j, (ushort)k]);
|
||||
for (int j = i + 1; j < Hand.Length; j++) {
|
||||
for (int k = j + 1; k < Hand.Length; k++) {
|
||||
var result = SetResult([(ushort)i, (ushort)j, (ushort)k]);
|
||||
|
||||
if (result == true) {
|
||||
Console.WriteLine($"Found set at {i}, {j}, {k}");
|
||||
res.Add([i, j, k]);
|
||||
}
|
||||
}
|
||||
if (result == true) {
|
||||
res.Add([i, j, k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold">Incorrect Matches</span>
|
||||
<span>{{ gameService.stats().fails }}</span>
|
||||
<span>{{ gameService.fails }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold">Hints Requested</span>
|
||||
<span>{{ gameService.stats().hints }}</span>
|
||||
<span>{{ gameService.hints }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,19 +67,19 @@
|
||||
|
||||
<p>
|
||||
<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>
|
||||
<span class="font-bold">Incorrect Matches</span>
|
||||
<span class="float-right">{{ gameService.stats().fails }}</span>
|
||||
<span class="float-right">{{ gameService.fails }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Hints Requested</span>
|
||||
<span class="float-right">{{ gameService.stats().hints }}</span>
|
||||
<span class="float-right">{{ gameService.hints }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<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>
|
||||
|
||||
<div class="mt-2">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ActivatedRoute, UrlSegment, Router } from '@angular/router';
|
||||
})
|
||||
|
||||
export class GameComponent {
|
||||
gameId: string = '';
|
||||
gameId: number = -1;
|
||||
hint: Card[] = []
|
||||
gameService: GameService = inject(GameService);
|
||||
route: ActivatedRoute = inject(ActivatedRoute);
|
||||
@@ -21,7 +21,7 @@ export class GameComponent {
|
||||
|
||||
constructor() {
|
||||
this.route.params.subscribe(params => {
|
||||
this.gameId = params['id'];
|
||||
this.gameId = parseInt(params['id']);
|
||||
|
||||
this.attachId(this.gameId);
|
||||
});
|
||||
@@ -31,10 +31,10 @@ export class GameComponent {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
async attachId(i: string) {
|
||||
async attachId(i: number) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ export class GameComponent {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +1,129 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Card, toCard } from '../../app/models/card';
|
||||
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' })
|
||||
export class GameService {
|
||||
private API_URL = 'http://localhost:5224/api/v1/Games';
|
||||
private http = inject(HttpClient);
|
||||
|
||||
public deck: Card[] = [];
|
||||
public hand: Card[] = [];
|
||||
public foundSets: Card[][] = [];
|
||||
public possibleSets: Card[][] = [];
|
||||
private API_URL = 'http://localhost:5224/api/v1/Games';
|
||||
|
||||
public fails: number = 0;
|
||||
public hints: number = 0;
|
||||
public deck: Card[] = [];
|
||||
public hand: Card[] = [];
|
||||
public selectedCards: Card[] = [];
|
||||
public foundSets: Card[][] = [];
|
||||
public possibleSets: Card[][] = [];
|
||||
|
||||
public fails: number = 0;
|
||||
public hints: number = 0;
|
||||
public gameId: number = 0;
|
||||
public startDate: Date = new Date();
|
||||
|
||||
public startDate: Date = new Date();
|
||||
public finishedAt?: Date;
|
||||
public selectedCards: Card[] = [];
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
) {}
|
||||
constructor() {}
|
||||
|
||||
public initGame(gameId?: string): Promise<string> {
|
||||
this.deck = [];
|
||||
this.hand = [];
|
||||
return gameId ? this.initializeExistingGame(gameId) : this.initializeDeck();
|
||||
public updateState(game: GameResponse): void {
|
||||
this.gameId = game.id;
|
||||
this.fails = game.fails;
|
||||
this.hints = game.hints;
|
||||
this.startDate = game.startedAt;
|
||||
this.finishedAt = game.finishedAt;
|
||||
|
||||
this.deck = game.deck.map(toCard);
|
||||
this.hand = game.hand.map(toCard);
|
||||
this.foundSets = [];
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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.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.gameId = response.id;
|
||||
this.updateSets();
|
||||
return response.id.toString();
|
||||
} catch (error) {
|
||||
console.error('Error initializing existing game', error);
|
||||
throw error;
|
||||
public async initGame(gameId?: number): Promise<number> {
|
||||
// Fetch game data from the server, use lastValueFrom to convert Observable to Promise
|
||||
let game: GameResponse;
|
||||
if (gameId) {
|
||||
game = await lastValueFrom(this.http.get<GameResponse>(`${this.API_URL}/${gameId}`))
|
||||
} else {
|
||||
game = await lastValueFrom(this.http.post<GameResponse>(this.API_URL, {}));
|
||||
}
|
||||
|
||||
// Update the game state with the fetched data
|
||||
this.updateState(game);
|
||||
|
||||
return game.id;
|
||||
}
|
||||
|
||||
private async initializeDeck(): Promise<string> {
|
||||
try {
|
||||
const response = await lastValueFrom(this.http.post<any>(this.API_URL, {}));
|
||||
|
||||
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.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();
|
||||
return response.id.toString();
|
||||
} 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}`)
|
||||
);
|
||||
|
||||
const cards = response.map((set: number[]) =>
|
||||
set.map((card: number) => this.hand[card])
|
||||
);
|
||||
|
||||
this.possibleSets = cards;
|
||||
return cards;
|
||||
} catch (error) {
|
||||
console.error('Error updating sets', error);
|
||||
throw error;
|
||||
}
|
||||
public async getSetsInHand(): Promise<void> {
|
||||
const sets = await lastValueFrom(
|
||||
this.http.get<number[][]>(`${this.API_URL}/SetsInHand/${this.gameId}`)
|
||||
);
|
||||
|
||||
this.possibleSets = sets.map(set => set.map(cardIndex => this.hand[cardIndex]));
|
||||
}
|
||||
|
||||
public async checkSet(cards: number[]): Promise<boolean> {
|
||||
try {
|
||||
const response = await lastValueFrom(
|
||||
this.http.post<any>(`${this.API_URL}/CheckSet/${this.gameId}`, cards)
|
||||
);
|
||||
|
||||
this.hand = response.newState.hand.map((card: number) => toCard(card));
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error('Error checking set', error);
|
||||
throw error;
|
||||
}
|
||||
const response = await lastValueFrom(
|
||||
this.http.post<SetCheckResponse>(`${this.API_URL}/CheckSet/${this.gameId}`, cards)
|
||||
);
|
||||
|
||||
this.updateState(response.newState);
|
||||
|
||||
return response.isSet;
|
||||
}
|
||||
|
||||
public selectCard(card: Card): void {
|
||||
const cardIndex = this.hand.indexOf(card);
|
||||
if (cardIndex === -1) return; // Card not found in hand
|
||||
// Card not found in hand
|
||||
if (this.hand.indexOf(card) === -1) return;
|
||||
|
||||
// Unselect card if already selected
|
||||
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) {
|
||||
this.selectedCards.push(card);
|
||||
}
|
||||
|
||||
// Check if 3 cards are selected. If so, check if they form a set
|
||||
if (this.selectedCards.length === 3) {
|
||||
const indices = this.selectedCards.map(c => this.hand.indexOf(c));
|
||||
this.checkSet(indices).then(isSet => {
|
||||
this.selectedCards = [];
|
||||
});
|
||||
this.checkSet(this.selectedCards.map(c => this.hand.indexOf(c)));
|
||||
this.selectedCards.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteGame(): Promise<void> {
|
||||
try {
|
||||
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<Card[]> {
|
||||
const hint = await lastValueFrom(this.http.get<HintResponse>(`${this.API_URL}/Hint/${this.gameId}`));
|
||||
this.hints++;
|
||||
|
||||
return hint.map((indexOfCard: number) => this.hand[indexOfCard]);
|
||||
}
|
||||
|
||||
public async showHint(): Promise<void> {
|
||||
try {
|
||||
await lastValueFrom(this.http.post<void>(`${this.API_URL}/Hint/${this.gameId}`, {}));
|
||||
this.hints++;
|
||||
} catch (error) {
|
||||
console.error('Error showing hint', error);
|
||||
throw error;
|
||||
}
|
||||
public deleteGame(): void {
|
||||
this.http.delete<void>(`${this.API_URL}/${this.gameId}`).subscribe();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user