Merge letters and numbers into a single countdown command
This commit is contained in:
parent
8400c52ab6
commit
a4178b89e7
8 changed files with 231 additions and 142 deletions
16
bin/countdown
Executable file
16
bin/countdown
Executable 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();
|
||||||
12
bin/letters
12
bin/letters
|
|
@ -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();
|
|
||||||
12
bin/numbers
12
bin/numbers
|
|
@ -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();
|
|
||||||
41
src/Commands/CountdownCommand.php
Normal file
41
src/Commands/CountdownCommand.php
Normal 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...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
93
src/Commands/LettersCommand.php
Normal file
93
src/Commands/LettersCommand.php
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/Commands/NumbersCommand.php
Normal file
81
src/Commands/NumbersCommand.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue