npm package: @rameneko/core
Description: Game environment for Rameneko — build your own AI copilot
Latest version: 0.1.0

# @rameneko/core

Game environment for Rameneko — build your own player for a ramen restaurant game.

## Quick Start

```ts
import { createEnv } from '@rameneko/core';

const env = createEnv({ level: 5 });

const obs = env.reset(42);           // seed for reproducibility
console.log(obs.state);              // WorldState — full game state
console.log(obs.validActions);       // number[] — what you can do right now

const result = env.step(3);          // pick action index 3
console.log(result.score);           // score delta for this tick
console.log(result.done);            // day over?
console.log(result.state.dayStats);  // { servedCount, failedCount, ... }
```

## How It Works

Each game is one "day" (90 ticks). You pick one action per tick. Customers arrive, place orders, and leave if not served in time. The goal: **serve as many customers as possible**.

### State

`obs.state` is the full `WorldState`. Key fields:

- `state.orders` — current customer orders (seat, recipe, patience)
- `state.bowls` — bowl assembly slots (broth, tare, noodle, toppings)
- `state.noodleSlots` — noodle boiler timers
- `state.preps` — cooking toppings in progress
- `state.timeLeft` / `state.dayDuration` — time remaining
- `state.combo` — current serve streak
- `state.dayStats` — running totals (servedCount, failedCount, earnings)
- `state.seatCans` — uncollected tips at seats

### Actions

Action 0 is always NOOP. Other actions:

- `START_NOODLE(slot)` — start boiling noodles
- `TAKE_NOODLE(noodle→bowl)` — move cooked noodles to a bowl
- `SET_BROTH(bowl, type)` — set broth on a bowl
- `SET_TARE(bowl, type)` — set tare on a bowl
- `START_TOPPING(type, bowl?)` — add instant topping or start cooking one
- `COLLECT_TOPPING(bowl)` — collect a cooked topping onto a bowl
- `SERVE(bowl→seat)` — serve a completed bowl to a customer
- `DISCARD_BOWL(bowl)` / `DISCARD_NOODLE(slot)` — discard
- `COLLECT_CANS(seat)` — collect tips from a seat

Use `env.actionNames` for the full list. Only actions in `obs.validActions` are valid — invalid actions are silently ignored.

### Scoring

| Event | Score |
|-------|-------|
| Serve a customer | +10 |
| Perfect noodle bonus | +3 |
| Combo multiplier | +1 × combo |
| Customer left | -5 |
| Serve wrong order | -1 |
| Discard a bowl | -0.5 |
| Per-tick penalty | -0.01 |

### Levels

| Level | Actions | Description |
|-------|---------|-------------|
| 1 | 19 | 1 broth, 1 tare, no toppings |
| 3 | 65 | 2 broths, 2 tares, basic toppings |
| 5 | 70 | Full broths/tares, more toppings |
| 7 | 77 | All toppings, more bowl/noodle slots |
| 10 | 77 | Everything unlocked |

## Player Interface

Implement the `Player` interface:

```ts
import { createEnv, type Player } from '@rameneko/core';

const myPlayer: Player = {
  decide(state, validActions) {
    // state: WorldState — inspect orders, bowls, noodles, etc.
    // validActions: number[] — pick one of these
    return validActions[Math.floor(Math.random() * validActions.length)];
  },
};

const env = createEnv({ level: 5 });
let obs = env.reset(42);
while (!env.done) {
  const action = myPlayer.decide(obs.state, obs.validActions);
  obs = env.step(action);
}
console.log('Served:', obs.state.dayStats.servedCount);
```

## Replays

Record a game as a replay, then share or play it back:

```ts
import { createEnv, simulateDay, type Player } from '@rameneko/core';

const env = createEnv({ level: 5 });
const replay = simulateDay(env, myPlayer, 42);
// replay: { seed, level, ticks: [{ tick, actions }] }

// Save as JSON for playback in the game viewer
import { writeFileSync } from 'fs';
writeFileSync('my-replay.json', JSON.stringify(replay));
```

## API Reference

### `createEnv({ level })`

Create a game environment for the given level.

### `GameEnv`

| Property | Type | Description |
|----------|------|-------------|
| `actionCount` | `number` | Total number of actions |
| `actionNames` | `string[]` | Human-readable action labels |
| `done` | `boolean` | Whether the day has ended |
| `tick` | `number` | Current tick (0-based) |

| Method | Returns | Description |
|--------|---------|-------------|
| `reset(seed?)` | `Observation` | Start a new day |
| `step(action)` | `StepResult` | Take one action, advance one tick |
| `step(actions[])` | `StepResult` | Take multiple actions in one tick |
| `getState()` | `WorldState` | Current game state |

### Types

```ts
interface Observation {
  state: WorldState;
  validActions: number[];
}

interface StepResult extends Observation {
  score: number;
  done: boolean;
}

interface Player {
  decide(state: WorldState, validActions: number[]): number;
  reset?(): void;
}

interface ReplayRecording {
  seed: number;
  level: number;
  ticks: { tick: number; actions: number[] }[];
}
```