17 KiB
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 artisancommands 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
newin controllers — no DI container injection (see existingCharacterController,GymController) - No PHPUnit in this project — manual verification via
./vendor/bin/sail artisan tinkerand 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): voidJailService::releaseFromJail(Character $character): voidJailService::calculateBustOutChance(Character $character): float(returns 0.0–1.0)
-
Step 1: Add two config keys to the
jailblock inconfig/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
$bustOutChanceand$bustOutCostto the view
- Route
-
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)$bustOutCostformatted withnumber_format()in the view
-
Step 1: Replace
resources/views/game/jail.blade.phpwith 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
/gamewith a green "You escaped! You're free." flash. - Failure path: You stay on
/game/jailwith 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"