├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── apps └── web │ ├── .eslintrc.js │ ├── README.md │ ├── app │ ├── [item] │ │ └── page.tsx │ ├── api │ │ ├── path │ │ │ └── [item] │ │ │ │ └── route.ts │ │ ├── precalculate │ │ │ └── [item] │ │ │ │ └── route.ts │ │ ├── recipes │ │ │ └── route.ts │ │ └── report │ │ │ └── route.ts │ ├── globals.css │ ├── icon.png │ ├── layout.tsx │ ├── page.tsx │ ├── precalculate │ │ └── page.tsx │ └── status │ │ └── page.tsx │ ├── components.json │ ├── components │ ├── App.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── Info.tsx │ ├── NotFound.tsx │ ├── Path.tsx │ ├── Precalculate.tsx │ ├── Results.tsx │ ├── Status.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ └── tooltip.tsx │ ├── lib │ ├── Finder.ts │ ├── compression.ts │ ├── items.ts │ ├── kv.ts │ ├── precalculatedPath.ts │ ├── status.ts │ └── utils.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── tailwind.config.js │ └── tsconfig.json ├── docker-compose.yml ├── package.json ├── packages ├── db │ ├── .eslintrc.js │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── prisma │ │ └── schema.prisma │ ├── tsconfig.json │ └── tsconfig.lint.json ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── worker │ ├── .eslintrc.js │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── src │ ├── Worker.ts │ └── types.ts │ ├── tsconfig.json │ └── tsconfig.lint.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@repo/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | .pnpm-store 8 | 9 | # Local env files 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # Testing 17 | coverage 18 | 19 | # Turbo 20 | .turbo 21 | 22 | # Vercel 23 | .vercel 24 | 25 | # Build Outputs 26 | .next/ 27 | out/ 28 | build 29 | dist 30 | 31 | 32 | # Debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Misc 38 | .DS_Store 39 | *.pem 40 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*prisma* 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 6 | RUN apk add --no-cache libc6-compat 7 | WORKDIR /app 8 | 9 | # Install dependencies based on the preferred package manager 10 | COPY . . 11 | RUN corepack enable pnpm && pnpm i --frozen-lockfile 12 | 13 | 14 | # Rebuild the source code only when needed 15 | FROM base AS builder 16 | WORKDIR /app 17 | COPY --from=deps /app/node_modules ./node_modules 18 | COPY . . 19 | 20 | # Next.js collects completely anonymous telemetry data about general usage. 21 | # Learn more here: https://nextjs.org/telemetry 22 | # Uncomment the following line in case you want to disable telemetry during the build. 23 | # ENV NEXT_TELEMETRY_DISABLED 1 24 | 25 | RUN corepack enable pnpm && pnpm run build 26 | 27 | # Production image, copy all the files and run next 28 | FROM base AS runner 29 | WORKDIR /app 30 | 31 | ENV NODE_ENV production 32 | # Uncomment the following line in case you want to disable telemetry during runtime. 33 | # ENV NEXT_TELEMETRY_DISABLED 1 34 | 35 | RUN addgroup --system --gid 1001 nodejs 36 | RUN adduser --system --uid 1001 nextjs 37 | 38 | COPY --from=builder /app/public ./public 39 | 40 | # Set the correct permission for prerender cache 41 | RUN mkdir .next 42 | RUN chown nextjs:nodejs .next 43 | 44 | # Automatically leverage output traces to reduce image size 45 | # https://nextjs.org/docs/advanced-features/output-file-tracing 46 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 47 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 48 | 49 | USER nextjs 50 | 51 | EXPOSE 3000 52 | 53 | ENV PORT 3000 54 | 55 | # server.js is created by next build from the standalone output 56 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 57 | CMD HOSTNAME="0.0.0.0" node server.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinite Craft Solver 2 | 3 | This project allows searching for the quickest way to craft a given item in the game [Infinite Craft](https://neal.fun/infinite-craft/). 4 | 5 | Visit to use the solver. 6 | 7 | ## Dataset 8 | 9 | Internally, this project uses a dataset of over 200k recipes fetched from the worker (see `packages/worker`). Feel free to download and use the dataset for your own projects. 10 | 11 | To use the dataset, download the compressed recipe list from . You can use the function in `apps/web/lib/compression.ts` to decompress the file and get an array of object in the format `{ first: string, second: string, result: string }`. As used by Infinite Craft, `first` and `second` are sorted alphabetically to allow for easier searching. 12 | 13 | ### Compression 14 | 15 | The dataset uses a simple compression method to reduce repetition of strings. For this, the compression file is a JSON object with the array "`items`" containing all unique item names and the array "`recipes`" containing all recipes. Each recipe is an array of three numbers, the first two being the index of the first and second item in the "`items`" array and the third being the index of the result item in the "`items`" array. 16 | 17 | With this, the dataset can be uncompressed by iterating over the "`recipes`" array and using the numbers to get the item names from the "`items`" array: 18 | 19 | ```ts 20 | function decompressRecipes({ 21 | items, 22 | recipes, 23 | }: { 24 | items: string[]; 25 | recipes: any[]; 26 | }): Recipe[] { 27 | return recipes.map((recipe) => ({ 28 | first: items[recipe[0]]!, 29 | second: items[recipe[1]]!, 30 | result: items[recipe[2]]!, 31 | })); 32 | } 33 | 34 | const recipes = decompressRecipes(compressedData); 35 | ``` 36 | 37 | ## Development 38 | 39 | The project is built using [TurboRepo](https://turbo.build/) and pnpm. To start the development server, run the following commands: 40 | 41 | ```sh 42 | pnpm install 43 | 44 | # Setup prisma 45 | pnpm dlx turbo db:generate db:push 46 | 47 | # Run the worker to fetch recipes 48 | pnpm dlx turbo worker:run 49 | 50 | # Start the frontend 51 | pnpm dlx turbo dev 52 | 53 | # Build production 54 | pnpm dlx turbo build 55 | 56 | # Run worker in background 57 | pm2 start "pnpm dlx turbo worker:run" 58 | 59 | # When running on a server, use xvfb to run the worker 60 | sudo apt-get install xvfb 61 | xvfb-run --auto-servernum pnpm dlx turbo worker:run 62 | pm2 start "xvfb-run --auto-servernum pnpm dlx turbo worker:run" --name ics-worker 63 | ``` 64 | 65 | The project needs a running PostgreSQL database to work. To set this up, create `.env` in `packages/db` and insert the configuration for a PostgreSQL database. 66 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | ## Infinite Craft Solver Frontend 2 | 3 | See the [main README](../../README.md) for more information about the project. 4 | -------------------------------------------------------------------------------- /apps/web/app/[item]/page.tsx: -------------------------------------------------------------------------------- 1 | import App from "@/components/App"; 2 | import React from "react"; 3 | 4 | function Item({ params: { item } }: { params: { item: string } }) { 5 | return ; 6 | } 7 | 8 | export default Item; 9 | -------------------------------------------------------------------------------- /apps/web/app/api/path/[item]/route.ts: -------------------------------------------------------------------------------- 1 | import redis from "@/lib/kv"; 2 | 3 | export async function GET( 4 | request: Request, 5 | { params: { item } }: { params: { item: string } } 6 | ) { 7 | const cachePath = `recipe-${item}`; 8 | const path = await redis.get(cachePath); 9 | return path ? Response.json(path) : new Response(null, { status: 404 }); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/api/precalculate/[item]/route.ts: -------------------------------------------------------------------------------- 1 | import redis from "@/lib/kv"; 2 | 3 | export async function POST( 4 | request: Request, 5 | { params: { item } }: { params: { item: string } } 6 | ) { 7 | if (process.env.NODE_ENV !== "development") { 8 | return new Response("Precalculate is only available in dev environments", { 9 | status: 404, 10 | }); 11 | } 12 | 13 | const data = await request.json(); 14 | const { path } = data; 15 | const cachePath = `recipe-${item}`; 16 | await redis.set(cachePath, path, { ex: 60 * 60 * 48 }); 17 | return new Response("ok"); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/api/recipes/route.ts: -------------------------------------------------------------------------------- 1 | export function GET() { 2 | return Response.redirect("https://icscdn.vantezzen.io/recipes.json", 301); 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/api/report/route.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from "@/lib/Finder"; 2 | import { decompressRecipes } from "@/lib/compression"; 3 | import redis from "@/lib/kv"; 4 | import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; 5 | 6 | export async function POST(request: Request) { 7 | const body = await request.json(); 8 | const { item, path } = body as { item: string; path: Recipe[] }; 9 | 10 | const client = new S3Client({ 11 | endpoint: process.env.S3_ENDPOINT, 12 | region: process.env.S3_REGION, 13 | credentials: { 14 | accessKeyId: process.env.S3_ACCESS_KEY!, 15 | secretAccessKey: process.env.S3_SECRET_KEY!, 16 | }, 17 | }); 18 | const command = new GetObjectCommand({ 19 | Bucket: process.env.S3_BUCKET!, 20 | Key: "recipes.json", 21 | }); 22 | const response = await client.send(command); 23 | 24 | if (!response.Body) { 25 | return new Response("Failed to load recipes", { status: 500 }); 26 | } 27 | 28 | const textData = await response.Body.transformToString(); 29 | const data = JSON.parse(textData); 30 | const recipes = decompressRecipes(data); 31 | 32 | const allRecipesValid = path.every((recipe) => { 33 | return recipes.find( 34 | (r) => 35 | r.first === recipe.first && 36 | r.second === recipe.second && 37 | r.result === recipe.result 38 | ); 39 | }); 40 | const finalItemCorrect = path[path.length - 1]!.result === item; 41 | 42 | if ( 43 | allRecipesValid && 44 | finalItemCorrect && 45 | path.length > 1 && 46 | path.length < 200 47 | ) { 48 | const cachePath = `recipe-${item}`; 49 | await redis.set(cachePath, path, { ex: 60 * 60 * 24 * 14 }); 50 | 51 | return new Response("ok", { status: 200 }); 52 | } 53 | 54 | return new Response("Invalid path", { status: 400 }); 55 | } 56 | -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /apps/web/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vantezzen/infinite-craft-solver/ea2c11f2323610c31666be9af09860287efb91c8/apps/web/app/icon.png -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import Script from "next/script"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Infinite Craft Solver", 10 | description: "Get the fastest path to craft any item in Infinite Craft", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }): JSX.Element { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import App from "@/components/App"; 2 | import React from "react"; 3 | 4 | function Page() { 5 | return ; 6 | } 7 | 8 | export default Page; 9 | -------------------------------------------------------------------------------- /apps/web/app/precalculate/page.tsx: -------------------------------------------------------------------------------- 1 | import Precalculate from "@/components/Precalculate"; 2 | import { getItemList } from "@/lib/items"; 3 | import React from "react"; 4 | 5 | async function PrecalculatePage() { 6 | if (process.env.NODE_ENV !== "development") { 7 | return
Precalculate is only available in dev environments
; 8 | } 9 | 10 | const items = await getItemList(); 11 | 12 | return ; 13 | } 14 | 15 | export default PrecalculatePage; 16 | -------------------------------------------------------------------------------- /apps/web/app/status/page.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/Footer"; 2 | import Status from "@/components/Status"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import React, { Suspense } from "react"; 5 | 6 | function StatusPage() { 7 | return ( 8 |
9 |
10 |
11 |
12 |

Infinite Craft Solver

13 |
14 |
15 | 16 | 19 | 20 |
21 | } 22 | > 23 | 24 | 25 |
26 |