Add shared multiplayer gear puzzle (Chamber of Motion)

Players open a no-login link and see live cursors and dragged gear
pieces synced in real time, backed by a small Workerman WebSocket
relay (src/Command/PuzzleRelayCommand.php) run alongside symfony
serve - no Docker/Mercure hub needed. The board renders the wall
artwork with a spare-gears tray, and a "Reset puzzle" button (visible
only to logged-in users, verified server-side via a signed token) lets
the DM wipe the shared state for everyone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-12 13:24:42 +02:00
co-authored by Claude Sonnet 5
parent 40dc30f950
commit 2ccbf4ca67
21 changed files with 3293 additions and 2394 deletions
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Puzzle\PuzzleResetTokenSigner;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* Shared, no-login puzzle screens: everyone with the link sees the same
* board, the same pieces, and each other's cursors, synced over the
* WebSocket relay in {@see \App\Command\PuzzleRelayCommand}.
*/
final class PuzzleController extends AbstractController
{
public function __construct(
#[Autowire(env: 'PUZZLE_RELAY_PUBLIC_URL')]
private readonly string $relayPublicUrl,
private readonly PuzzleResetTokenSigner $resetTokenSigner,
) {
}
#[Route('/puzzle/{room}', name: 'app_puzzle_room', requirements: ['room' => '[a-z0-9-]+'], methods: ['GET'])]
public function room(string $room): Response
{
return $this->render('puzzle/room.html.twig', [
'room' => $room,
'relayPublicUrl' => $this->relayPublicUrl,
// Only a logged-in user gets a token that lets them reset the shared
// board; anonymous players can play but not wipe it for everyone.
'resetToken' => $this->getUser() ? $this->resetTokenSigner->sign($room) : null,
]);
}
}