Files
thecrimesynticate/docs/superpowers/plans/2026-06-28-escape-from-jail.md

17 KiB
Raw Permalink Blame History

Escape from Jail Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a Bust Out mechanic to the jail page — spend 1000 cash, roll against a dexterity+speed formula, succeed to escape or fail to extend your sentence by 1.5×.

Architecture: A new JailService holds all bust-out logic (mirrors the existing HealthService::revive() pattern). A new JailController handles the HTTP action. The existing jail view gains a Bust Out card with a CSRF form. No new database migrations.

Tech Stack: Laravel 11, PHP 8.2, Blade templating, Eloquent ORM, Laravel Sail (Docker). All artisan commands must be run via ./vendor/bin/sail artisan.

Global Constraints

  • Project runs in Laravel Sail — all php artisan commands must use ./vendor/bin/sail artisan
  • Dark crime theme: Tailwind classes bg-syndicate-*, text-crime-*, bg-crime-*, text-yellow-* — match the existing jail page colour palette (yellow for jail, not purple/green)
  • Services are instantiated with new in controllers — no DI container injection (see existing CharacterController, GymController)
  • No PHPUnit in this project — manual verification via ./vendor/bin/sail artisan tinker and browser testing only
  • Flash messages use session()->flash('success', ...) / session()->flash('error', ...); displayed via existing layout partials
  • CSRF required on all POST forms (@csrf)
  • Routes file: routes/game.php

File Map

Action File
Modify config/game.php — add two keys to jail block
Create app/Services/Game/JailService.php
Create app/Http/Controllers/Game/JailController.php
Modify routes/game.php — add bust-out route and pass new vars to jail view
Modify resources/views/game/jail.blade.php — add Bust Out card

Task 1: Config + JailService

Files:

  • Modify: config/game.php (lines 81–86)
  • Create: app/Services/Game/JailService.php

Interfaces:

  • Produces:

    • JailService::bustOut(Character $character): array{success: bool, message: string}
    • JailService::sendToJail(Character $character, int $seconds): void
    • JailService::releaseFromJail(Character $character): void
    • JailService::calculateBustOutChance(Character $character): float (returns 0.0–1.0)
  • Step 1: Add two config keys to the jail block in config/game.php

Open config/game.php. The existing jail block is at lines 81–86:

// Jail system configuration
'jail' => [
    'sentence_length'          => 3600,
    'breakout_chance'          => 0.05,
    'breakout_cost'            => 1000,
    'sentence_multiplier'      => 1.5,
    'bust_out_stat_multiplier' => 0.003,  // per combined dex+speed point
    'bust_out_max_chance'      => 0.80,   // cap at 80%
],

Replace the existing block (4 keys) with the 6-key version above.

  • Step 2: Verify config is readable
./vendor/bin/sail artisan tinker

In the tinker prompt:

config('game.jail.bust_out_stat_multiplier'); // expect 0.003
config('game.jail.bust_out_max_chance');      // expect 0.8
exit
  • Step 3: Create app/Services/Game/JailService.php
<?php

namespace App\Services\Game;

use App\Models\Character;

class JailService
{
    /**
     * Calculate bust-out success chance for a character (0.0 to 1.0).
     */
    public function calculateBustOutChance(Character $character): float
    {
        $base       = config('game.jail.breakout_chance', 0.05);
        $multiplier = config('game.jail.bust_out_stat_multiplier', 0.003);
        $cap        = config('game.jail.bust_out_max_chance', 0.80);

        return min($cap, $base + ($character->dexterity + $character->speed) * $multiplier);
    }

    /**
     * Attempt to bust out of jail.
     *
     * Deducts breakout_cost cash regardless of outcome.
     * On success: releases character immediately.
     * On failure: multiplies remaining sentence by sentence_multiplier.
     *
     * @return array{success: bool, message: string}
     */
    public function bustOut(Character $character): array
    {
        $cost = config('game.jail.breakout_cost', 1000);

        if ($character->money < $cost) {
            return [
                'success' => false,
                'message' => 'You need $' . number_format($cost) . ' to attempt a bust-out.',
            ];
        }

        $character->money -= $cost;
        $successRate = $this->calculateBustOutChance($character);
        $roll = random_int(0, 9999) / 10000;

        if ($roll <= $successRate) {
            $this->releaseFromJail($character);
            $character->save();

            return [
                'success' => true,
                'message' => "You escaped! You're free.",
            ];
        }

        // Failure: extend sentence
        $multiplier = config('game.jail.sentence_multiplier', 1.5);
        $remaining  = max(0, $character->in_jail_until->diffInSeconds(now()));
        $extended   = (int) ceil($remaining * $multiplier);

        $character->in_jail_until = now()->addSeconds($extended);
        $character->save();

        $newReleaseTime = $character->in_jail_until->diffForHumans();

        return [
            'success' => false,
            'message' => "Bust failed — your sentence was extended to {$newReleaseTime}.",
        ];
    }

    /**
     * Send a character to jail for the given number of seconds.
     */
    public function sendToJail(Character $character, int $seconds): void
    {
        $character->status       = 'in_jail';
        $character->in_jail_until = now()->addSeconds($seconds);
        $character->save();
    }

    /**
     * Release a character from jail immediately.
     */
    public function releaseFromJail(Character $character): void
    {
        $character->status        = 'active';
        $character->in_jail_until = null;
    }
}
  • Step 4: Smoke-test the service in tinker
./vendor/bin/sail artisan tinker
use App\Services\Game\JailService;
use App\Models\Character;

$svc = new JailService();

// Fake a character with known stats
$char = new Character(['dexterity' => 5, 'speed' => 5]);
$chance = $svc->calculateBustOutChance($char);
// expect: 0.05 + (5+5)*0.003 = 0.08
echo $chance; // 0.08

$char2 = new Character(['dexterity' => 50, 'speed' => 50]);
echo $svc->calculateBustOutChance($char2); // 0.35

$char3 = new Character(['dexterity' => 100, 'speed' => 100]);
echo $svc->calculateBustOutChance($char3); // 0.8 (capped)
exit
  • Step 5: Commit
git add config/game.php app/Services/Game/JailService.php
git commit -m "feat: add JailService and bust-out config keys"

Task 2: JailController + Route

Files:

  • Create: app/Http/Controllers/Game/JailController.php
  • Modify: routes/game.php

Interfaces:

  • Consumes:

    • JailService::bustOut(Character): array{success: bool, message: string} (Task 1)
    • JailService::calculateBustOutChance(Character): float (Task 1)
  • Produces:

    • Route game.jail.bust-out (POST /game/jail/bust-out)
    • Updated jail view route passes $bustOutChance and $bustOutCost to the view
  • Step 1: Create app/Http/Controllers/Game/JailController.php

<?php

namespace App\Http\Controllers\Game;

use App\Http\Controllers\Controller;
use App\Services\Game\JailService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class JailController extends Controller
{
    protected JailService $jailService;

    public function __construct()
    {
        $this->jailService = new JailService();
    }

    /**
     * Attempt to bust out of jail.
     */
    public function bustOut(Request $request): RedirectResponse
    {
        $character = $request->user()->character;

        if (!$character || !$character->isInJail()) {
            return redirect()->route('game.dashboard');
        }

        $result = $this->jailService->bustOut($character);

        if ($result['success']) {
            return redirect()->route('game.dashboard')
                ->with('success', $result['message']);
        }

        return redirect()->route('game.jail')
            ->with('error', $result['message']);
    }
}
  • Step 2: Add the bust-out route and update the jail GET route in routes/game.php

The existing jail route group starts at line 81:

// Jail route (for when character is in jail)
Route::middleware(['auth'])->group(function () {
    Route::get('/game/jail', function () {
        $character = auth()->user()->character;
        if (!$character || !$character->isInJail()) {
            return redirect()->route('game.dashboard');
        }
        $releaseTime    = $character->in_jail_until ? $character->in_jail_until->diffForHumans() : 'unknown';
        $bustOutChance  = (int) round((new \App\Services\Game\JailService())->calculateBustOutChance($character) * 100);
        $bustOutCost    = config('game.jail.breakout_cost', 1000);
        return view('game.jail', compact('character', 'releaseTime', 'bustOutChance', 'bustOutCost'));
    })->name('game.jail');

    Route::post('/game/jail/bust-out', [\App\Http\Controllers\Game\JailController::class, 'bustOut'])
        ->name('game.jail.bust-out');
});

Replace the existing jail group (lines 81–91 of routes/game.php) with the block above.

  • Step 3: Verify routes are registered
./vendor/bin/sail artisan route:list --name=game.jail

Expected output includes two rows:

GET  /game/jail           game.jail
POST /game/jail/bust-out  game.jail.bust-out
  • Step 4: Commit
git add app/Http/Controllers/Game/JailController.php routes/game.php
git commit -m "feat: add JailController and bust-out route"

Task 3: Jail View — Bust Out Card

Files:

  • Modify: resources/views/game/jail.blade.php

Interfaces:

  • Consumes (variables passed from the route closure, Task 2):

    • $character — Character model (has ->money)
    • $releaseTime — string, e.g. "in 45 minutes"
    • $bustOutChance — int (percentage, e.g. 8 for 8%)
    • $bustOutCost — int (e.g. 1000)
    • $bustOutCost formatted with number_format() in the view
  • Step 1: Replace resources/views/game/jail.blade.php with the updated version

@extends('layouts.game')

@section('title', 'Jail')

@section('game-content')
    <div class="max-w-2xl mx-auto space-y-4">

        {{-- Flash messages --}}
        @if(session('success'))
            <div class="bg-green-900/30 border border-green-700 text-green-300 rounded-lg px-4 py-3 text-sm">
                {{ session('success') }}
            </div>
        @endif
        @if(session('error'))
            <div class="bg-red-900/30 border border-red-700 text-red-300 rounded-lg px-4 py-3 text-sm">
                {{ session('error') }}
            </div>
        @endif

        <div class="bg-syndicate-800 border border-syndicate-700 rounded-lg">
            <div class="p-8 text-center">
                <div class="mb-6">
                    <span class="text-6xl">🚔</span>
                </div>

                <h1 class="font-display text-2xl font-bold text-gray-100 mb-4">You're in Jail</h1>
                <p class="text-gray-400 mb-6 text-sm">Your character has been arrested and is currently serving time.</p>

                {{-- Status card --}}
                <div class="bg-yellow-900/20 border border-yellow-800 rounded-lg p-6 mb-6 text-left">
                    <h3 class="font-display text-base font-semibold text-yellow-300 mb-3">Your Status</h3>
                    <div class="space-y-2 text-sm">
                        <div class="flex justify-between">
                            <span class="text-gray-400">Status</span>
                            <span class="text-yellow-400 font-medium">In Jail</span>
                        </div>
                        <div class="flex justify-between">
                            <span class="text-gray-400">Release Time</span>
                            <span class="text-gray-200 font-medium">{{ $releaseTime }}</span>
                        </div>
                        <div class="flex justify-between">
                            <span class="text-gray-400">Cash on Hand</span>
                            <span class="text-gray-200 font-medium">${{ number_format($character->money) }}</span>
                        </div>
                    </div>
                </div>

                {{-- Bust Out card --}}
                <div class="bg-syndicate-700 border border-syndicate-600 rounded-lg p-6 mb-6 text-left">
                    <h3 class="font-display text-base font-semibold text-gray-100 mb-1">Bust Out</h3>
                    <p class="text-gray-400 text-xs mb-4">Spend cash for a chance to escape early. Failure extends your sentence.</p>

                    <div class="space-y-2 text-sm mb-4">
                        <div class="flex justify-between">
                            <span class="text-gray-400">Success Chance</span>
                            <span class="text-yellow-400 font-medium">{{ $bustOutChance }}%</span>
                        </div>
                        <div class="flex justify-between">
                            <span class="text-gray-400">Cost</span>
                            <span class="text-gray-200 font-medium">${{ number_format($bustOutCost) }}</span>
                        </div>
                        <div class="flex justify-between">
                            <span class="text-gray-400">On Failure</span>
                            <span class="text-red-400 font-medium">Sentence × 1.5</span>
                        </div>
                    </div>

                    @if($character->money >= $bustOutCost)
                        <form action="{{ route('game.jail.bust-out') }}" method="POST">
                            @csrf
                            <button type="submit"
                                class="w-full flex items-center justify-center px-6 py-3 bg-yellow-700 hover:bg-yellow-600 text-white font-medium rounded-lg transition">
                                <span class="mr-2">🔓</span>
                                Bust Out ({{ $bustOutChance }}% chance)
                            </button>
                        </form>
                    @else
                        <button type="button" disabled
                            title="You need ${{ number_format($bustOutCost) }} to attempt a bust-out"
                            class="w-full flex items-center justify-center px-6 py-3 bg-syndicate-600 text-gray-500 font-medium rounded-lg cursor-not-allowed">
                            <span class="mr-2">🔒</span>
                            Not Enough Cash
                        </button>
                    @endif
                </div>

                <div class="space-y-4">
                    <p class="text-sm text-gray-600">Or wait for your sentence to complete automatically.</p>

                    <a href="{{ route('game.dashboard') }}"
                       class="inline-flex items-center text-gray-500 hover:text-gray-300 transition text-sm">
                        <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
                        </svg>
                        Back to Dashboard
                    </a>
                </div>
            </div>
        </div>
    </div>
@endsection
  • Step 2: Verify the jail page renders

In a browser (or via curl), navigate to /game/jail while logged in with a jailed character. Confirm:

  • Status card shows release time and cash on hand
  • Bust Out card shows correct % and cost
  • If character has ≥ $1,000: button is active (yellow), form submits to game.jail.bust-out
  • If character has < $1,000: button is disabled, grey, shows tooltip

To temporarily put a character in jail for testing:

./vendor/bin/sail artisan tinker
use App\Models\Character;
$c = Character::first();
$c->status = 'in_jail';
$c->in_jail_until = now()->addHour();
$c->save();
exit

Then visit /game/jail.

  • Step 3: Test the full bust-out flow

With the jailed character, click "Bust Out":

  • Success path: You are redirected to /game with a green "You escaped! You're free." flash.
  • Failure path: You stay on /game/jail with a red "Bust failed — your sentence was extended to..." flash and the release time has increased.

Verify cash was deducted in both cases:

./vendor/bin/sail artisan tinker
use App\Models\Character;
Character::first()->money; // should be 1000 less than before
exit
  • Step 4: Commit
git add resources/views/game/jail.blade.php
git commit -m "feat: add bust-out card to jail page"