mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 17:12:08 +00:00
build the backend structure and the fe for the Game
This commit is contained in:
@@ -1,11 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreRoomRequest;
|
||||
use App\Models\Deck;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class RoomController extends Controller
|
||||
{
|
||||
public function create(){
|
||||
return Inertia::render('rooms/CreateRoom');
|
||||
public function create(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$decks = Deck::query()
|
||||
->where(function ($query) use ($user) {
|
||||
$query->where('is_system', true);
|
||||
|
||||
if ($user !== null) {
|
||||
$query->orWhere('user_id', $user->id);
|
||||
}
|
||||
})
|
||||
->orderBy('name')
|
||||
->get([
|
||||
'id',
|
||||
'name',
|
||||
'cards',
|
||||
]);
|
||||
|
||||
return Inertia::render('rooms/Create', [
|
||||
'decks' => $decks,
|
||||
'defaultPlayerName' => $user?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, StoreRoomRequest $roomStoreRequest)
|
||||
{
|
||||
$validated = $roomStoreRequest->validated();
|
||||
$user = $request->user();
|
||||
|
||||
$deck = Deck::query()
|
||||
->whereKey($validated['deck_id'])
|
||||
->where(function ($query) use ($user) {
|
||||
$query->where('is_system', true);
|
||||
|
||||
if ($user !== null) {
|
||||
$query->orWhere('user_id', $user->id);
|
||||
}
|
||||
})
|
||||
->firstOrFail();
|
||||
|
||||
[$room, $player] = DB::transaction(function () use (
|
||||
$validated,
|
||||
$deck,
|
||||
$user,
|
||||
) {
|
||||
$room = Room::create([
|
||||
'title' => $validated['title'] ?? null,
|
||||
'deck_config' => [
|
||||
'id' => $deck->id,
|
||||
'name' => $deck->name,
|
||||
'cards' => $deck->cards,
|
||||
],
|
||||
]);
|
||||
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user?->id,
|
||||
'name' => $validated['player_name'],
|
||||
'is_moderator' => true,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
return [$room, $player];
|
||||
});
|
||||
|
||||
$request->session()->put(
|
||||
"room_players.{$room->id}",
|
||||
$player->id,
|
||||
);
|
||||
|
||||
return to_route('room.show', $room);
|
||||
}
|
||||
|
||||
public function show(Room $room, Request $request)
|
||||
{
|
||||
$currentPlayer = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless($currentPlayer instanceof Player, 403);
|
||||
|
||||
$room->load([
|
||||
'players' => fn ($query) => $query
|
||||
->select(['id', 'room_id', 'name', 'is_moderator', 'created_at'])
|
||||
->orderByDesc('is_moderator')
|
||||
->orderBy('created_at'),
|
||||
'players.votes' => fn ($query) => $query
|
||||
->where('room_id', $room->id)
|
||||
->where('round', $room->current_round),
|
||||
]);
|
||||
|
||||
$players = $room->players->map(function (Player $player) use ($room) {
|
||||
$vote = $player->votes->first();
|
||||
|
||||
return [
|
||||
'id' => $player->id,
|
||||
'name' => $player->name,
|
||||
'is_moderator' => $player->is_moderator,
|
||||
'has_voted' => $vote !== null,
|
||||
'vote' => $room->revealed_at !== null ? $vote?->value : null,
|
||||
];
|
||||
});
|
||||
|
||||
$currentVote = $room->players
|
||||
->firstWhere('id', $currentPlayer->id)
|
||||
?->votes
|
||||
->first();
|
||||
|
||||
return Inertia::render('rooms/Show', [
|
||||
'room' => [
|
||||
'id' => $room->id,
|
||||
'title' => $room->title,
|
||||
'join_code' => $room->join_code,
|
||||
'current_round' => $room->current_round,
|
||||
'revealed' => $room->revealed_at !== null,
|
||||
'state_version' => $room->state_version,
|
||||
'deck' => [
|
||||
'name' => $room->deck_config['name'],
|
||||
'cards' => $room->deck_config['cards'],
|
||||
],
|
||||
'players' => $players,
|
||||
],
|
||||
'currentPlayer' => [
|
||||
'id' => $currentPlayer->id,
|
||||
'name' => $currentPlayer->name,
|
||||
'is_moderator' => $currentPlayer->is_moderator,
|
||||
'current_vote' => $currentVote?->value,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function join(Request $request)
|
||||
{
|
||||
return Inertia::render('rooms/Join', [
|
||||
'join_code' => $request->query('join_code'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\JoinRoomRequest;
|
||||
use App\Models\Room;
|
||||
|
||||
class RoomPlayerController extends Controller
|
||||
{
|
||||
public function join(JoinRoomRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$user = $request->user();
|
||||
|
||||
$room = Room::where('join_code', '=', $validated['join_code'])->firstOrFail();
|
||||
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user?->id,
|
||||
'name' => $validated['player_name'],
|
||||
'is_moderator' => false,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
$request->session()->put(
|
||||
"room_players.{$room->id}",
|
||||
$player->id,
|
||||
);
|
||||
|
||||
return to_route('room.show', $room);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RoundController extends Controller
|
||||
{
|
||||
public function reset(
|
||||
Request $request,
|
||||
Room $room,
|
||||
): RedirectResponse {
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
$player instanceof Player
|
||||
&& $player->room_id === $room->id
|
||||
&& $player->is_moderator,
|
||||
403,
|
||||
);
|
||||
|
||||
DB::transaction(function () use ($room) {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
Vote::query()
|
||||
->where('room_id', $lockedRoom->id)
|
||||
->where('round', $lockedRoom->current_round)
|
||||
->delete();
|
||||
|
||||
$lockedRoom->update([
|
||||
'revealed_at' => null,
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function reveal(
|
||||
Request $request,
|
||||
Room $room,
|
||||
): RedirectResponse {
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
$player instanceof Player
|
||||
&& $player->room_id === $room->id
|
||||
&& $player->is_moderator,
|
||||
403,
|
||||
);
|
||||
|
||||
DB::transaction(function () use ($room) {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
if ($lockedRoom->revealed_at !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'room' => 'Die Karten sind bereits aufgedeckt.',
|
||||
]);
|
||||
}
|
||||
|
||||
$lockedRoom->update([
|
||||
'revealed_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function nextRound(
|
||||
Request $request,
|
||||
Room $room,
|
||||
): RedirectResponse {
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
$player instanceof Player
|
||||
&& $player->room_id === $room->id
|
||||
&& $player->is_moderator,
|
||||
403,
|
||||
);
|
||||
|
||||
DB::transaction(function () use ($room) {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
if ($lockedRoom->revealed_at === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'room' => 'Die aktuelle Runde muss zuerst aufgedeckt werden.',
|
||||
]);
|
||||
}
|
||||
|
||||
$lockedRoom->update([
|
||||
'current_round' => $lockedRoom->current_round + 1,
|
||||
'revealed_at' => null,
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,55 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\SubmitVoteRequest;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VoteController extends Controller
|
||||
{
|
||||
//
|
||||
public function store(
|
||||
SubmitVoteRequest $request,
|
||||
Room $room,
|
||||
): RedirectResponse {
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
$player instanceof Player
|
||||
&& $player->room_id === $room->id,
|
||||
403,
|
||||
);
|
||||
|
||||
$value = $request->validated('value');
|
||||
|
||||
DB::transaction(function () use ($room, $player, $value) {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
if ($lockedRoom->revealed_at !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'value' => 'Die Karten wurden bereits aufgedeckt.',
|
||||
]);
|
||||
}
|
||||
|
||||
Vote::updateOrCreate(
|
||||
[
|
||||
'room_id' => $lockedRoom->id,
|
||||
'player_id' => $player->id,
|
||||
'round' => $lockedRoom->current_round,
|
||||
],
|
||||
[
|
||||
'value' => $value,
|
||||
],
|
||||
);
|
||||
|
||||
$lockedRoom->increment('state_version');
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Room;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureRoomParticipant
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$room = $request->route('room');
|
||||
|
||||
if (! $room instanceof Room) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$sessionKey = "room_players.{$room->id}";
|
||||
$playerId = $request->session()->get($sessionKey);
|
||||
|
||||
if (! $playerId) {
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
])->withErrors([
|
||||
'join_code' => 'Du musst diesen Raum erst beitreten.',
|
||||
]);
|
||||
}
|
||||
|
||||
$player = $room->players()->find($playerId);
|
||||
|
||||
if (! $player) {
|
||||
$request->session()->forget($sessionKey);
|
||||
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
]);
|
||||
}
|
||||
|
||||
$request->attributes->set('currentPlayer', $player);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class JoinRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'join_code' => strtoupper(trim((string) $this->input('join_code'))),
|
||||
'player_name' => trim((string) $this->input('player_name')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$roomId = Room::query()
|
||||
->where('join_code', $this->input('join_code'))
|
||||
->value('id');
|
||||
|
||||
return [
|
||||
'join_code' => ['bail', 'string', 'required', 'size:6', Rule::exists(Room::class, 'join_code')],
|
||||
'player_name' => ['required', 'string', 'max:50', Rule::unique(Player::class, 'name')->where('room_id', $roomId)],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'join_code.exists' => 'Dieser Raumcode existiert nicht.',
|
||||
'join_code.size' => 'Der Raumcode muss aus sechs Zeichen bestehen.',
|
||||
'player_name.unique' => 'Dieser Name wird in dem Raum bereits verwendet.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['nullable', 'string', 'max:100'],
|
||||
'player_name' => ['required', 'string', 'max:50'],
|
||||
'deck_id' => ['required', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class SubmitVoteRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$room = $this->route('room');
|
||||
|
||||
return [
|
||||
'value' => ['bail', 'required', 'string', Rule::in(array_column($room->deck_config['cards'], 'key'))],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'value.required' => 'Du musst eine Karte auswählen.',
|
||||
'value.in' => 'Diese Karte gehört nicht zum verwendeten Deck.',
|
||||
];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@ class Deck extends Model
|
||||
{
|
||||
return [
|
||||
'cards' => 'array',
|
||||
'is_system' => 'boolean'
|
||||
'is_system' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use phpDocumentor\Reflection\Types\Boolean;
|
||||
|
||||
class Player extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'room_id',
|
||||
'room_id',
|
||||
'user_id',
|
||||
'name',
|
||||
'is_moderator',
|
||||
'last_seen_at',
|
||||
@@ -58,7 +58,7 @@ class Player extends Model
|
||||
return $this->is_moderator;
|
||||
}
|
||||
|
||||
public function markAsSeen(): void
|
||||
public function markAsSeen(): void
|
||||
{
|
||||
$this->update([
|
||||
'last_seen_at' => now(),
|
||||
|
||||
+5
-8
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Override;
|
||||
|
||||
class Room extends Model
|
||||
{
|
||||
@@ -20,7 +18,7 @@ class Room extends Model
|
||||
'state_version',
|
||||
];
|
||||
|
||||
protected function casts() :array
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'current_round' => 'integer',
|
||||
@@ -37,15 +35,15 @@ class Room extends Model
|
||||
});
|
||||
}
|
||||
|
||||
private static function generateJoinCode(): string
|
||||
private static function generateJoinCode(): string
|
||||
{
|
||||
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
|
||||
do {
|
||||
$code = '';
|
||||
|
||||
for($i = 0; $i < 6; $i++) {
|
||||
$code .= $alphabet[random_int(0,strlen($alphabet)-1)];
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
} while (static::where('join_code', $code)->exists());
|
||||
|
||||
@@ -53,14 +51,13 @@ class Room extends Model
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function players(): HasMany
|
||||
{
|
||||
return $this->hasMany(Player::class);
|
||||
|
||||
}
|
||||
|
||||
public function vote(): HasMany
|
||||
public function votes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
|
||||
+6
-5
@@ -11,22 +11,23 @@ class Vote extends Model
|
||||
'round',
|
||||
'value',
|
||||
'room_id',
|
||||
'player_id'
|
||||
'player_id',
|
||||
];
|
||||
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'round' => 'integer',
|
||||
'round' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function room(): BelongsTo {
|
||||
public function room(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Room::class);
|
||||
}
|
||||
|
||||
public function player(): BelongsTo {
|
||||
public function player(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Player::class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user