Merge letters and numbers into a single countdown command

This commit is contained in:
myrmidex 2026-09-01 00:52:25 +02:00
parent 8400c52ab6
commit a4178b89e7
8 changed files with 231 additions and 142 deletions

16
bin/countdown Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env php
<?php
require __DIR__ . '/../autoload.php';
use Lvl0\Countdown\TUI\Commands\CountdownCommand;
use Lvl0\Countdown\TUI\Commands\LettersCommand;
use Lvl0\Countdown\TUI\Commands\NumbersCommand;
use Symfony\Component\Console\Application;
$application = new Application('Countdown Solver', '0.1.3');
$application->addCommand(new CountdownCommand());
$application->addCommand(new LettersCommand());
$application->addCommand(new NumbersCommand());
$application->setDefaultCommand('countdown');
$application->run();

View file

@ -1,12 +0,0 @@
#!/usr/bin/env php
<?php
require __DIR__ . '/../autoload.php';
use Lvl0\Countdown\TUI\Commands\FindAnagramWordsCommand;
use Symfony\Component\Console\Application;
$application = new Application('Countdown Letters Solver', '0.1.3');
$application->addCommand(new FindAnagramWordsCommand());
$application->setDefaultCommand('countdown:letters');
$application->run();

View file

@ -1,12 +0,0 @@
#!/usr/bin/env php
<?php
require __DIR__ . '/../autoload.php';
use Lvl0\Countdown\TUI\Commands\FindNumbersCommand;
use Symfony\Component\Console\Application;
$application = new Application('Countdown Numbers Solver', '0.1.3');
$application->addCommand(new FindNumbersCommand());
$application->setDefaultCommand('countdown:numbers');
$application->run();

View file

@ -0,0 +1,41 @@
<?php
namespace Lvl0\Countdown\TUI\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use function Laravel\Prompts\clear;
use function Laravel\Prompts\info;
use function Laravel\Prompts\pause;
use function Laravel\Prompts\select;
#[AsCommand(name: 'countdown')]
class CountdownCommand extends Command
{
public function __invoke(): int
{
clear();
while (true) {
info("COUNTDOWN SOLVER");
$game = select('Which round?', [
'letters' => 'letters',
'numbers' => 'numbers',
'quit' => 'Quit',
]);
if ($game === 'quit') {
return Command::SUCCESS;
}
match ($game) {
'letters' => new LettersCommand()(null, false, false),
'numbers' => new NumbersCommand()(),
};
pause('Press enter to return to the menu...');
}
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace Lvl0\Countdown\TUI\Commands;
use Lvl0\Countdown\Letters\Matcher;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use function Laravel\Prompts\clear;
use function Laravel\Prompts\info;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\table;
use function Laravel\Prompts\text;
#[AsCommand(name: 'countdown:letters')]
class FindAnagramWordsCommand extends Command
{
public function __invoke(
#[Argument]
?string $letters,
#[Argument]
bool $all = false,
#[Option]
bool $single = false,
): int {
clear();
info("ANAGRAM FINDER");
$letters = $letters ??= text("Enter the letters", required: true);
info("Results for your letters: " . $letters);
$matches = spin(
callback: fn() => Matcher::findWords($letters),
message: 'Checking dictionary...',
);
if ($all) {
$matches = $matches->slice(0, 10);
}
if ($single) {
$matches = $matches->first();
}
table(
headers: ["word", "length"],
rows: $matches->map(fn(string $word) => ['word' => $word, 'length' => strlen($word)])->toArray(),
);
return Command::SUCCESS;
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Lvl0\Countdown\TUI\Commands;
use Illuminate\Support\Collection;
use Lvl0\Countdown\Numbers\Dtos\NumbersBag;
use Lvl0\Countdown\Numbers\Dtos\Step;
use Lvl0\Countdown\Numbers\Services\NumbersService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use function Laravel\Prompts\callout;
use function Laravel\Prompts\clear;
use function Laravel\Prompts\error;
use function Laravel\Prompts\intro;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\text;
#[AsCommand(name: 'countdown:numbers')]
class FindNumbersCommand extends Command
{
public function __invoke(): int
{
clear();
intro("NUMBERS FINDER");
$numbersInput = text("Enter the numbers", required: true);
$numbers = new NumbersBag(
new Collection(preg_split('/\s+/', trim($numbersInput), -1, PREG_SPLIT_NO_EMPTY))
->filter(fn (string $number) => ctype_digit($number))
->map(fn (string $number) => intval($number))
);
$target = text(label: "Enter the target number", required: true);
if (! ctype_digit($target)) {
error("Invalid target number");
return self::FAILURE;
}
$target = intval($target);
$result = spin(
callback: fn() => new NumbersService($numbers)->solve($target),
message: 'Calculating...',
);
$distance = (abs($target - $result->getResult()));
callout(
label: $distance === 0 ? 'Found!' : "Off by {$distance}",
content: $result->getSteps()->map(fn(Step $s) => $s->toString())->all(),
type: $distance === 0 ? 'success' : 'warning',
info: 'Steps needed to get to the solution',
);
return self::SUCCESS;
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Lvl0\Countdown\TUI\Commands;
use Illuminate\Support\Collection;
use Lvl0\Countdown\Letters\Matcher;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use function Laravel\Prompts\callout;
use function Laravel\Prompts\form;
use function Laravel\Prompts\note;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\table;
use function Laravel\Prompts\title;
#[AsCommand(name: 'countdown:letters')]
class LettersCommand extends Command
{
private const TOP = 10;
public function __invoke(): int
{
title("Letters Round");
$responses = form()
->text(
label: 'Your nine letters',
placeholder: 'raisedon',
required: 'Enter the letters you were given.',
validate: fn(string $value) => match (true) {
! ctype_alpha($this->normalise($value)) => 'Letters only, no digits or symbols',
strlen($this->normalise($value)) < 3 => 'Enter at least three letters',
default => null,
},
hint: 'With or without spaces',
name: 'letters',
)
->submit();
$letters = $this->normalise($responses['letters']);
note(sprintf('Solving %s', strtoupper($letters)));
$matches = spin(
callback: fn() => Matcher::findWords($letters),
message: 'Checking dictionary...',
);
if ($matches->isEmpty()) {
callout(
label: 'No words found',
content: 'Nothing in the dictionary matches those letters.',
type: 'warning',
);
return self::SUCCESS;
}
$top = $this->rank($matches)->take(self::TOP);
$best = strlen($top->first());
callout(
label: "Best: {$top->first()} ({$best} letters)",
content: sprintf('%d words found', $matches->count()),
type: $best >= 8 ? 'success' : 'info',
);
table(
headers: ['#', 'Word', 'Length'],
rows: $top->values()->map(fn(string $word, int $i) => [
$i + 1,
$word,
strlen($word),
])->all(),
);
return self::SUCCESS;
}
private function rank(Collection $matches): Collection
{
return $matches->sortBy([
fn(string $a, string $b) => strlen($b) <=> strlen($a),
fn(string $a, string $b) => $a <=> $b,
]);
}
private function normalise(string $input): string
{
return strtolower(preg_replace('/\s+/', '', trim($input)));
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Lvl0\Countdown\TUI\Commands;
use Illuminate\Support\Collection;
use Lvl0\Countdown\Numbers\Dtos\NumbersBag;
use Lvl0\Countdown\Numbers\Dtos\Step;
use Lvl0\Countdown\Numbers\Services\NumbersService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use function Laravel\Prompts\callout;
use function Laravel\Prompts\form;
use function Laravel\Prompts\note;
use function Laravel\Prompts\number;
use function Laravel\Prompts\pause;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\title;
#[AsCommand(name: 'countdown:numbers')]
class NumbersCommand extends Command
{
public function __invoke(): int
{
title('Numbers Round');
$responses = form()
->text(
label: 'Your six numbers',
placeholder: '25 50 75 100 3 6',
required: 'Enter the numbers you were given.',
validate: fn(string $value) => $this->parse($value)->count() < 2
? 'Enter at least two numbers, separated by spaces'
: null,
hint: 'Separated by spaces',
name: 'numbers',
)
->add(
fn(array $responses, $previous) => number(
label: 'Target',
placeholder: '952',
required: 'Enter the target number.',
hint: 'The number to reach',
min: 1,
max: 999,
),
name: 'target',
)
->submit();
$numbers = $this->parse($responses['numbers']);
$target = (int) $responses['target'];
note(sprintf('Solving %s -> %d', $numbers->implode(' '), $target));
$result = spin(
callback: fn() => new NumbersService(new NumbersBag($numbers))->solve($target),
message: 'Searching for a solution...',
);
$distance = (abs($target - $result->getResult()));
callout(
label: $distance === 0
? "Exactly {$target}"
: "{$result->getResult()} - off by {$distance}",
content: $result->getSteps()->map(fn(Step $s) => $s->toString())->all(),
type: $distance === 0 ? 'success' : 'warning',
info: $result->getSteps()->count() . ' steps',
);
return self::SUCCESS;
}
private function parse(string $input): Collection
{
return new Collection(preg_split('/\s+/', trim($input), -1, PREG_SPLIT_NO_EMPTY))
->filter(fn(string $number) => ctype_digit($number))
->map(fn(string $number) => (int) $number);
}
}