From fea6e18d573873c93036c7cfec4a9f10be3dd3d5 Mon Sep 17 00:00:00 2001 From: ia7ck <23146842+ia7ck@users.noreply.github.com> Date: Wed, 13 Mar 2024 02:37:58 +0900 Subject: [PATCH] abc323_f --- app/abc323_f/layout.tsx | 9 + app/abc323_f/page.tsx | 558 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 567 insertions(+) create mode 100644 app/abc323_f/layout.tsx create mode 100644 app/abc323_f/page.tsx diff --git a/app/abc323_f/layout.tsx b/app/abc323_f/layout.tsx new file mode 100644 index 0000000..eb65139 --- /dev/null +++ b/app/abc323_f/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "ABC323 F Visualizer", +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/app/abc323_f/page.tsx b/app/abc323_f/page.tsx new file mode 100644 index 0000000..ec769e1 --- /dev/null +++ b/app/abc323_f/page.tsx @@ -0,0 +1,558 @@ +"use client"; + +import { CubeIcon, FlagIcon, UserIcon } from "@heroicons/react/16/solid"; +import { ArrowUturnLeftIcon } from "@heroicons/react/16/solid"; +import { StrictMode, useEffect, useState } from "react"; +import { z } from "zod"; + +const X_MIN = -5; +const X_MAX = 5; +const Y_MIN = X_MIN; +const Y_MAX = X_MAX; + +const PositionX = z.coerce.number().int().min(X_MIN).max(X_MAX); +const PositionY = z.coerce.number().int().min(Y_MIN).max(Y_MAX); +const XY = z.object({ x: PositionX, y: PositionY }); +type XY = z.infer; + +const StartPositions = z + .object({ player: XY, cargo: XY, goal: XY }) + .refine(({ player, cargo }) => player.x !== cargo.x || player.y !== cargo.y, { + message: "Player and cargo cannot be at the same position.", + path: ["player", "cargo"], + }) + .refine(({ cargo, goal }) => cargo.x !== goal.x || cargo.y !== goal.y, { + message: "Cargo and goal cannot be at the same position.", + path: ["cargo", "goal"], + }); + +export default function ABC323_F() { + const [playerXText, setPlayerXText] = useState("1"); + const [playerYText, setPlayerYText] = useState("2"); + const [cargoXText, setCargoXText] = useState("3"); + const [cargoYText, setCargoYText] = useState("3"); + const [goalXText, setGoalXText] = useState("0"); + const [goalYText, setGoalYText] = useState("5"); + + const [player, setPlayer] = useState({ x: 1, y: 2 }); + const [cargo, setCargo] = useState({ x: 3, y: 3 }); + const [goal, setGoal] = useState({ x: 0, y: 5 }); + + const [actions, setActions] = useState(0); + const [minActions, setMinActions] = useState( + minimumActions(player, cargo, goal), + ); + + const startPositions = StartPositions.safeParse({ + player: { x: playerXText, y: playerYText }, + cargo: { x: cargoXText, y: cargoYText }, + goal: { x: goalXText, y: goalYText }, + }); + console.log(startPositions); + const finish = cargo.x === goal.x && cargo.y === goal.y; + + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + const nextPositions = listNextPositions(player, cargo); + let nextPosition: undefined | { player: XY; cargo: XY }; + if (e.key === "w") { + nextPosition = nextPositions.find( + ({ player: { x, y } }) => x === player.x && y === player.y + 1, + ); + } else if (e.key === "a") { + nextPosition = nextPositions.find( + ({ player: { x, y } }) => x === player.x - 1 && y === player.y, + ); + } else if (e.key === "s") { + nextPosition = nextPositions.find( + ({ player: { x, y } }) => x === player.x && y === player.y - 1, + ); + } else if (e.key === "d") { + nextPosition = nextPositions.find( + ({ player: { x, y } }) => x === player.x + 1 && y === player.y, + ); + } + if (nextPosition) { + handleCellClick(nextPosition); + } + } + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + // useEffectEvent を使うと依存値いらなくなるかも + // https://ja.react.dev/learn/separating-events-from-effects + }, [player, cargo]); + + function handleGachaButtonClick() { + const player = { + x: getRandomInt(X_MIN, X_MAX + 1), + y: getRandomInt(Y_MIN, Y_MAX + 1), + }; + const cargo = { + x: getRandomInt(X_MIN, X_MAX + 1), + y: getRandomInt(Y_MIN, Y_MAX + 1), + }; + const goal = { + x: getRandomInt(X_MIN, X_MAX + 1), + y: getRandomInt(Y_MIN, Y_MAX + 1), + }; + while (!StartPositions.safeParse({ player, cargo, goal }).success) { + cargo.x = getRandomInt(X_MIN, X_MAX + 1); + cargo.y = getRandomInt(Y_MIN, Y_MAX + 1); + } + + setPlayerXText(player.x.toString()); + setPlayerYText(player.y.toString()); + setCargoXText(cargo.x.toString()); + setCargoYText(cargo.y.toString()); + setGoalXText(goal.x.toString()); + setGoalYText(goal.y.toString()); + + setPlayer(player); + setCargo(cargo); + setGoal(goal); + + setActions(0); + setMinActions(minimumActions(player, cargo, goal)); + } + + function handleCellClick(nextPosition?: { player: XY; cargo: XY }) { + if (!finish && nextPosition) { + setPlayer(nextPosition.player); + setCargo(nextPosition.cargo); + + const distance = + Math.abs(nextPosition.player.x - player.x) + + Math.abs(nextPosition.player.y - player.y); + setActions(actions + distance); + } + } + + function handleResetButtonClick() { + if (startPositions.success) { + const { player, cargo, goal } = startPositions.data; + + setPlayer(player); + setCargo(cargo); + setGoal(goal); + + setActions(0); + setMinActions(minimumActions(player, cargo, goal)); + } + } + + return ( + +
+
+

+ Problem +

+ + ABC323 F - Push and Carry + +
+ +
+
+ {range(Y_MIN - 1, Y_MAX + 1) + .toReversed() + .map((y) => ( +
+ {range(X_MIN - 1, X_MAX + 1).map((x) => { + let icon = null; + if (x === player.x && y === player.y) { + icon = ; + } else if (x === cargo.x && y === cargo.y) { + icon = ; + } else if (x === goal.x && y === goal.y) { + icon = ; + } + const nextP = listNextPositions(player, cargo).find( + ({ player }) => player.x === x && player.y === y, + ); + return ( + // biome-ignore lint/a11y/useKeyWithClickEvents: ;-; + handleCellClick(nextP)} + > + {icon} + + ); + })} +
+ ))} +
+
+

+ You can move with WASD keys. +

+

+ actions: + + {actions} {finish && actions <= minActions && "🎉"} + +

+

+ best: + {minActions} +

+
+
+ +
+

+ {X_MIN} ≦ XA, XB, XC ≦ {X_MAX} +
+ {Y_MIN} ≦ YA, YB, YC ≦ {Y_MAX} +
+ (XA, YA) ≠ (XB, YB) +
+ (XB, YB) ≠ (XC, YC) +

+ + + (issue.path.includes("player") && issue.path.includes("x")) || + (issue.path.includes("player") && + issue.path.includes("cargo")), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={playerXText} + onChange={(e) => setPlayerXText(e.target.value)} + /> + + + (issue.path.includes("player") && issue.path.includes("y")) || + (issue.path.includes("player") && + issue.path.includes("cargo")), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={playerYText} + onChange={(e) => setPlayerYText(e.target.value)} + /> + + + issue.path.includes("cargo") && issue.path.includes("x"), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={cargoXText} + onChange={(e) => setCargoXText(e.target.value)} + /> + + + issue.path.includes("cargo") && issue.path.includes("y"), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={cargoYText} + onChange={(e) => setCargoYText(e.target.value)} + /> + + + (issue.path.includes("goal") && issue.path.includes("x")) || + (issue.path.includes("goal") && issue.path.includes("cargo")), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={goalXText} + onChange={(e) => setGoalXText(e.target.value)} + /> + + + (issue.path.includes("goal") && issue.path.includes("y")) || + (issue.path.includes("goal") && issue.path.includes("cargo")), + ) + ? "text-red-900 ring-red-300 focus:ring-red-500" + : "" + }`} + value={goalYText} + onChange={(e) => setGoalYText(e.target.value)} + /> +
+
+
+ ); +} + +function listNextPositions(player: XY, cargo: XY): { player: XY; cargo: XY }[] { + const result: { player: XY; cargo: XY }[] = []; + + for (let y = player.y + 1; y <= Y_MAX + 1; y++) { + if (player.x === cargo.x && y === cargo.y) { + if (y + 1 <= Y_MAX + 1) { + result.push({ + player: { ...player, y }, + cargo: { ...cargo, y: y + 1 }, + }); + } + break; + } + result.push({ player: { ...player, y }, cargo }); + } + + for (let y = player.y - 1; y >= Y_MIN - 1; y--) { + if (player.x === cargo.x && y === cargo.y) { + if (y - 1 >= Y_MIN - 1) { + result.push({ + player: { ...player, y }, + cargo: { ...cargo, y: y - 1 }, + }); + } + break; + } + result.push({ player: { ...player, y }, cargo }); + } + + for (let x = player.x + 1; x <= X_MAX + 1; x++) { + if (x === cargo.x && player.y === cargo.y) { + if (x + 1 <= X_MAX + 1) { + result.push({ + player: { ...player, x }, + cargo: { ...cargo, x: x + 1 }, + }); + } + break; + } + result.push({ player: { ...player, x }, cargo }); + } + + for (let x = player.x - 1; x >= X_MIN - 1; x--) { + if (x === cargo.x && player.y === cargo.y) { + if (x - 1 >= X_MIN - 1) { + result.push({ + player: { ...player, x }, + cargo: { ...cargo, x: x - 1 }, + }); + } + break; + } + result.push({ player: { ...player, x }, cargo }); + } + + return result; +} + +// https://atcoder.jp/contests/abc323/submissions/51186675 +function minimumActions(player: XY, cargo: XY, goal: XY): number { + function solve(player: XY, cargo: XY, goal: XY): number { + const cargo_ = { x: cargo.x - player.x, y: cargo.y - player.y }; + const goal_ = { x: goal.x - player.x, y: goal.y - player.y }; + if (cargo_.x < 0) { + cargo_.x *= -1; + goal_.x *= -1; + } + if (cargo_.y < 0) { + cargo_.y *= -1; + goal_.y *= -1; + } + + // cargo_.x >= 0 + // cargo_.y >= 0 + // cargo_ != { x: 0, y: 0 } + // cargo_ != goal_ + + let newPlayer: XY; + let newCargo: XY; + let actions: number; + if (cargo_.x < goal_.x) { + newPlayer = { x: goal_.x - 1, y: cargo_.y }; + newCargo = { x: goal_.x, y: cargo_.y }; + actions = + cargo_.x === 0 ? 1 + cargo_.y + goal_.x : cargo_.y + (goal_.x - 1); + } else if (goal_.x < cargo_.x) { + newPlayer = { x: goal_.x + 1, y: cargo_.y }; + newCargo = { x: goal_.x, y: cargo_.y }; + actions = + cargo_.y === 0 + ? 1 + (cargo_.x + 1) + 1 + (cargo_.x - goal_.x) + : cargo_.x + 1 + cargo_.y + (cargo_.x - goal_.x); + } else { + newPlayer = { x: 0, y: 0 }; + newCargo = { ...cargo_ }; + actions = 0; + } + + // newPlayer.x === goal_.x + + if (newPlayer.y === goal_.y) { + return actions; + } + + return ( + actions + + solve( + { x: newPlayer.y, y: newPlayer.x }, + { x: newCargo.y, y: newCargo.x }, + { x: goal_.y, y: goal_.x }, + ) + ); + } + + return Math.min( + solve(player, cargo, goal), + solve( + { x: player.y, y: player.x }, + { x: cargo.y, y: cargo.x }, + { x: goal.y, y: goal.x }, + ), + ); +} + +// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from +function range(start: number, end: number): number[] { + return Array.from({ length: end - start + 1 }, (_, i) => start + i); +} + +// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random +function getRandomInt(min: number, max: number) { + const min_ = Math.ceil(min); + const max_ = Math.floor(max); + return Math.floor(Math.random() * (max_ - min_) + min_); +}