Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions kaggle_environments/envs/kaggriculture/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Kaggriculture is a two-player farming sim. Each player manages a farm and compet
- **Weeds** — every empty unlocked tile has a `weedSpawnChance` (default 0.005) of spawning a weed at end-of-day; clear with `DIG`
- **Shed** — non-seed inventory cap of 100 items. Items beyond the cap at end-of-day drop are discarded. Seeds live in their own slot (no cap, never picked up by `PICKUP` — `PLANT` consumes them directly)
- **Market** — fixed prices for seeds, animals, and `BUY_PRODUCT` orders; sale prices for harvested produce vary dynamically with market inventory. Price is `base` at the shared starting inventory `I0`, rises as inventory falls, and falls as inventory grows, using a per-resource shape function (`linear`, `sq`, `sqrt`, or `log`) that can differ on each side of `I0` — so gluts hit premium goods (strawberry, melon, milk, wool) hard, driving them to the $1 floor, while staples absorb oversupply more gently (see the Price Function table in [README.md](README.md)). Only wheat and fertilizer can be bought back via `BUY_PRODUCT`; every product can be sold via `SELL`. Each turn, at most `maxMarketOrdersPerTurn` (default 10) orders are processed per player; extras are silently dropped
- **Town** — town center always demands product (1 of each non-fertilizer product every `townCenterSellInterval` turns, default 12, scaling to 2× after day 10 and 4× after day 20). Additional shops unlock every `townShopUnlockInterval` days (default 3, random selection from the remaining pool); each unlocked shop consumes one of every product it demands every `townShopSellInterval` turns (default 4, single-product shops consume 2×) — see the Town Buildings table in [README.md](README.md)
- **Town** — town center always demands product (1 of each non-fertilizer product every `townCenterSellInterval` turns, default 24 — once per day, flat for the whole season). Additional shops unlock every `townShopUnlockInterval` days (default 3, drawn uniformly at random **with replacement**, so duplicates are possible; capped at 8 instances); each unlocked shop instance consumes one of every product it demands every `townShopSellInterval` turns (default 4, single-product shops consume 2×) — see the Town Buildings table in [README.md](README.md)
- **Season length** — 24 turns per day × 30 days = 720 turns by default
- **Win condition** — most coins in the bank at the end of the season; ties are possible

Expand All @@ -46,7 +46,7 @@ Your agent is a function that receives an observation and returns an action dict
- `market` — shared:
- `inventory` — `{product: int}` current market supply
- `prices` — `{product: int}` current per-unit sale price (rounded, floor 1)
- `town` — shared: `unlocked_shops` — list of currently-active shop names
- `town` — shared: `unlocked_shops` — list of currently-active shop names; names may repeat (shops are drawn with replacement) and each entry consumes independently

**Action format:**

Expand Down
14 changes: 7 additions & 7 deletions kaggle_environments/envs/kaggriculture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ The shed sits at the center of the board and is not a tile — it never appears

### Town Buildings

As the season progresses, new shops unlock at regular intervals (every `townShopUnlockInterval` days, default 3). Each unlock is randomly selected from the shops that have not yet been added; once unlocked, a shop stays active for the rest of the game. Total demand grows monotonically as more shops unlock.
As the season progresses, new shops unlock at regular intervals (every `townShopUnlockInterval` days, default 3). Each unlock is drawn uniformly at random **with replacement** from the full shop table, so the same shop can unlock more than once — a season might end up with three bakeries and no yarn store. Once unlocked, a shop stays active for the rest of the game, and unlocking stops after 8 total instances. Total demand grows monotonically as more shops unlock.

Each unlocked shop consumes one of every product it demands every `townShopSellInterval` turns (default 4). So with the default interval, a shop demanding wheat removes 6 wheat from the market per day. Single-product shops consume 2x.
Each unlocked shop *instance* consumes one of every product it demands every `townShopSellInterval` turns (default 4). So with the default interval, a shop demanding wheat removes 6 wheat from the market per day, and two copies of that shop remove 12. Single-product shops consume 2x.

In addition, the town center consumes one of every product (excluding fertilizer) every `townCenterSellInterval` turns (default 12). After day 10 this is increased to 2 of each, and after day 20 it is increased to 4 of each.
In addition, the town center consumes one of every product (excluding fertilizer) every `townCenterSellInterval` turns (default 24, i.e. once per day). This rate is flat for the whole season — it does not ramp.

| Shop Type | Increases Demand For |
| :---- | :---- |
Expand Down Expand Up @@ -268,7 +268,7 @@ The top-level observation passed to each agent:
"prices": { "WHEAT": int, "CARROT": int, ... },
},
"town": { # shared
"unlocked_shops": ["BAKERY", ...],
"unlocked_shops": ["BAKERY", "BAKERY", ...], # may repeat; each entry consumes independently
},
"private": { # this player only; opponent's private state is not visible
"shed": { "WHEAT": int, "GOOSE": int, "FERTILIZER": int, ... },
Expand Down Expand Up @@ -355,8 +355,8 @@ Per-crop seed costs and per-product base prices are not configurable; they are d
| turnsPerDay | 24 | Number of turns that make up one in-game day |
| shedCapacity | 100 | Max non-seed items the shed can hold; overflow at end-of-day drop is discarded |
| weedSpawnChance | 0.005 | Per-tile probability of a weed spawning on an empty unlocked tile during end-of-day refresh |
| townShopUnlockInterval | 3 | Days between successive town shop unlocks |
| townShopSellInterval | 4 | Turns between consumption ticks by every unlocked town shop |
| townCenterSellInterval | 12 | Turns between consumption ticks by the town center |
| townShopUnlockInterval | 3 | Days between successive town shop unlocks (drawn with replacement, capped at 8 instances) |
| townShopSellInterval | 4 | Turns between consumption ticks by every unlocked town shop instance |
| townCenterSellInterval | 24 | Turns between consumption ticks by the town center (flat rate, once per day) |
| seed | null | Optional input seed for deterministic episode generation; cleared from config after read so it stays out of agent observations |

8 changes: 4 additions & 4 deletions kaggle_environments/envs/kaggriculture/kaggriculture.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@
"minimum": 0
},
"townShopUnlockInterval": {
"description": "Days between successive town shop unlocks. First shop unlocks on day equal to this value.",
"description": "Days between successive town shop unlocks. First shop unlocks on day equal to this value. Shops are drawn with replacement, so the same shop can unlock more than once; unlocking stops after 8 instances.",
"type": "integer",
"default": 3,
"minimum": 1
},
"townShopSellInterval": {
"description": "Number of turns between successive consumption ticks by every unlocked town shop. Each shop pulls one of each of its products per tick (single-product shops pull 2x). Total demand grows monotonically as more shops are unlocked.",
"description": "Number of turns between successive consumption ticks by every unlocked town shop instance. Each instance pulls one of each of its products per tick (single-product shops pull 2x), so a duplicated shop consumes once per copy. Total demand grows monotonically as more shops are unlocked.",
"type": "integer",
"default": 4,
"minimum": 1
},
"townCenterSellInterval": {
"description": "Number of turns between successive consumption ticks by the town center (one of every product per tick).",
"description": "Number of turns between successive consumption ticks by the town center (one of every non-fertilizer product per tick). At the default of 24 with turnsPerDay 24, the town center buys once per day at a flat rate for the whole season.",
"type": "integer",
"default": 12,
"default": 24,
"minimum": 1
},
"seed": {
Expand Down
23 changes: 12 additions & 11 deletions kaggle_environments/envs/kaggriculture/kaggriculture.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ def _resolve_market_params(overrides):

TOWN_CENTER_PRODUCTS = [p for p in PRODUCTS if p != "FERTILIZER"]

# Town center demand schedule: (day_threshold, multiplier), highest threshold first.
TOWN_CENTER_DEMAND_SCHEDULE = [(20, 4), (10, 2), (0, 1)]
# Maximum number of shop instances the town will ever unlock. Shops are drawn
# with replacement, so this caps total count, not variety.
MAX_SHOP_INSTANCES = 8


def get(d, key, default):
Expand Down Expand Up @@ -717,21 +718,20 @@ def _town_consume(env, state, step):
town = obs0.town
cfg = env.configuration
shop_interval = max(1, int(get(cfg, "townShopSellInterval", 4)))
center_interval = max(1, int(get(cfg, "townCenterSellInterval", 12)))
turns_per_day = max(1, int(get(cfg, "turnsPerDay", 24)))
day = step // turns_per_day
center_interval = max(1, int(get(cfg, "townCenterSellInterval", 24)))

if step % shop_interval == 0:
# unlocked_shops may list the same shop more than once (shops are drawn
# with replacement); each instance consumes independently.
for shop_name in town.get("unlocked_shops", []):
products = SHOPS[shop_name]
multiplier = 2 if len(products) == 1 else 1
for item in products:
market["inventory"][item] -= multiplier

if step % center_interval == 0:
center_mult = next(m for threshold, m in TOWN_CENTER_DEMAND_SCHEDULE if day >= threshold)
for item in TOWN_CENTER_PRODUCTS:
market["inventory"][item] -= center_mult
market["inventory"][item] -= 1

_refresh_prices(market)

Expand Down Expand Up @@ -871,10 +871,11 @@ def _end_of_day(state, env, day):
next_day = day + 1
town = obs0.town
if next_day > 0 and next_day % shop_interval == 0:
remaining = [s for s in SHOPS if s not in town["unlocked_shops"]]
if remaining:
choice = rng.choice(sorted(remaining))
town["unlocked_shops"].append(choice)
# Drawn with replacement: the same shop can unlock repeatedly, and each
# copy consumes independently. Variety is not guaranteed; only the total
# instance count is capped.
if len(town["unlocked_shops"]) < MAX_SHOP_INSTANCES:
town["unlocked_shops"].append(rng.choice(sorted(SHOPS)))


def interpreter(state, env):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
MARKET_ITEMS,
QUADRANT_BY_SEGMENT,
SEGMENT,
SURROUNDING_BUILDINGS,
SHOP_BUILDINGS,
TOWN_CENTER_INDEX,
TOWN_EMPTY_BRICK_INDICES,
TOWN_GRID_COLS,
TOWN_GRID_ROWS,
TOWN_SHOP_SLOT_ORDER,
TOWN_SIGN_INDEX,
type BoardSize,
type CellRefs,
Expand Down Expand Up @@ -187,10 +188,12 @@ function townPanel(): string {
<img class="town-sprite" src="${spriteSrc('town_sign')}" alt="" title="Welcome to Kaggriculture!" />
</div>`;
}
const building = SURROUNDING_BUILDINGS[i];
if (building) {
// renderTown injects the shop sprite once the shop unlocks.
return `<div class="town-slot town-slot--shop" data-slot="${i}" data-building="${building.shop}" style="${BG_BRICK_SLOT}"></div>`;
const shopOrder = TOWN_SHOP_SLOT_ORDER.indexOf(i);
if (shopOrder !== -1) {
// Slots are generic: renderTown fills them in unlock order, so the
// n-th unlocked shop lands in the n-th slot regardless of which
// shop it is.
return `<div class="town-slot town-slot--shop" data-slot="${i}" data-shop-order="${shopOrder}" style="${BG_BRICK_SLOT}"></div>`;
}
if (TOWN_EMPTY_BRICK_INDICES.has(i)) {
return `<div class="town-slot" data-slot="${i}" style="${BG_BRICK_SLOT}"></div>`;
Expand Down Expand Up @@ -303,7 +306,10 @@ export function collectRefs(root: HTMLElement, board: BoardSize): LayoutRefs {
dayValues: Array.from(root.querySelectorAll<HTMLElement>('.day-value')),
turnValues: Array.from(root.querySelectorAll<HTMLElement>('.turn-value')),
marketItems,
shopSlots: Array.from(root.querySelectorAll<HTMLElement>('.town-slot--shop')),
// Sorted by fill order so shopSlots[n] is where the n-th unlocked shop goes.
shopSlots: Array.from(root.querySelectorAll<HTMLElement>('.town-slot--shop')).sort(
(a, b) => Number(a.dataset.shopOrder) - Number(b.dataset.shopOrder)
),
townGeese: Array.from(root.querySelectorAll<HTMLImageElement>('.town-goose')),
players: [1, 2].map((p) =>
collectPlayerRefs(root.querySelector<HTMLElement>(`.farm-panel[data-player="${p}"]`)!, board)
Expand Down Expand Up @@ -612,30 +618,35 @@ function renderMarket(refs: LayoutRefs, market: MarketPublic, priceHistory: Reco
}

function renderTown(refs: LayoutRefs, town: TownPublic): void {
const active = new Set(town?.unlocked_shops ?? []);
// Look up by interpreter shop key.
const buildingByShop = new Map<string, { sprite: string; label: string }>();
for (const b of Object.values(SURROUNDING_BUILDINGS)) buildingByShop.set(b.shop, b);

for (const slot of refs.shopSlots) {
const shop = slot.dataset.building ?? '';
const isActive = active.has(shop);
const meta = buildingByShop.get(shop);
// Shops are drawn with replacement, so unlocked_shops may repeat a name.
// Slots are generic and filled in unlock order, so every instance gets its
// own building -- three farmers' markets show as three sprites.
const unlocked = town?.unlocked_shops ?? [];

refs.shopSlots.forEach((slot, i) => {
const shop = unlocked[i];
const meta = shop ? SHOP_BUILDINGS[shop] : undefined;
const existing = slot.querySelector<HTMLImageElement>('.town-sprite');
if (isActive && meta) {
if (!existing) {
const img = document.createElement('img');
img.className = 'town-sprite';
img.src = spriteSrc(meta.sprite);
img.alt = meta.label;
img.title = meta.label;
// Insert before flower overlays so flowers stay on top.
slot.insertBefore(img, slot.firstChild);
}
} else if (existing) {
existing.remove();
if (!meta) {
if (existing) existing.remove();
return;
}
}
const src = spriteSrc(meta.sprite);
if (!existing) {
const img = document.createElement('img');
img.className = 'town-sprite';
img.src = src;
img.alt = meta.label;
img.title = meta.label;
// Insert before flower overlays so flowers stay on top.
slot.insertBefore(img, slot.firstChild);
} else if (existing.getAttribute('src') !== src) {
// Scrubbing backwards can leave a different shop in this slot.
existing.src = src;
existing.alt = meta.label;
existing.title = meta.label;
}
});
}

function renderGeese(refs: LayoutRefs, day: number, hour: number): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export const SEGMENT = 5;
// always empty.
export const INVENTORY_SLOTS = 18;

// 3x4 grid: top row holds the bakery, town center, and pizza shop. The
// town sign sits on bare grass at row 2 middle, with a grass-empty slot
// below it and a brick-empty slot at the bottom of the middle column.
// The remaining 6 shops line the left and right columns. Shops unlock
// one per 3 in-game days.
// 3x4 grid: the town center sits at row 1 middle and the town sign on bare
// grass at row 2 middle, with a grass-empty slot below it and a brick-empty
// slot at the bottom of the middle column. The remaining 8 slots hold shops,
// filled in unlock order (see TOWN_SHOP_SLOT_ORDER). Shops unlock one per 3
// in-game days.
export const TOWN_GRID_COLS = 3;
export const TOWN_GRID_ROWS = 4;
export const TOWN_CENTER_INDEX = 1;
Expand All @@ -26,20 +26,26 @@ export const QUADRANT_BY_SEGMENT: Record<number, string> = {
3: 'SE',
};

// Shop slot index in the 3x4 grid (skipping center=1, sign=4, grass-empty
// cell 7, and brick-empty cell 10) -> { interpreter shop key, sprite name,
// label }.
export const SURROUNDING_BUILDINGS: Record<number, { shop: string; sprite: string; label: string }> = {
0: { shop: 'BAKERY', sprite: 'bakery', label: 'Bakery' },
2: { shop: 'PIZZA_SHOP', sprite: 'pizza', label: 'Pizza Shop' },
3: { shop: 'BRUNCH_SPOT', sprite: 'brunch', label: 'Brunch Spot' },
5: { shop: 'YARN_STORE', sprite: 'yarn', label: 'Yarn Store' },
6: { shop: 'ICE_CREAM_SHOP', sprite: 'icecream', label: 'Ice Cream Shop' },
8: { shop: 'PET_CAFE', sprite: 'petcafe', label: 'Pet Cafe' },
9: { shop: 'SMOOTHIE_SHOP', sprite: 'smoothie', label: 'Smoothie Shop' },
11: { shop: 'FARMERS_MARKET', sprite: 'farmersmarket', label: "Farmers' Market" },
// Interpreter shop key -> { sprite name, label }. Slot position is not fixed
// per shop: shops are drawn with replacement, so the same shop can unlock
// several times and each instance gets its own slot.
export const SHOP_BUILDINGS: Record<string, { sprite: string; label: string }> = {
BAKERY: { sprite: 'bakery', label: 'Bakery' },
PIZZA_SHOP: { sprite: 'pizza', label: 'Pizza Shop' },
BRUNCH_SPOT: { sprite: 'brunch', label: 'Brunch Spot' },
YARN_STORE: { sprite: 'yarn', label: 'Yarn Store' },
ICE_CREAM_SHOP: { sprite: 'icecream', label: 'Ice Cream Shop' },
PET_CAFE: { sprite: 'petcafe', label: 'Pet Cafe' },
SMOOTHIE_SHOP: { sprite: 'smoothie', label: 'Smoothie Shop' },
FARMERS_MARKET: { sprite: 'farmersmarket', label: "Farmers' Market" },
};

// Grid indices that hold shops, in the order they get filled as shops unlock:
// down the left column and right column together, skipping center=1, sign=4,
// grass-empty=7, and brick-empty=10. Length must be >= the interpreter's
// MAX_SHOP_INSTANCES (8) so every unlocked instance has a home.
export const TOWN_SHOP_SLOT_ORDER: readonly number[] = [0, 2, 3, 5, 6, 8, 9, 11];

// Visible market items. `key` is the interpreter's PRODUCTS key; `sprite` is the asset name.
export const MARKET_ITEMS: { sprite: string; key: string }[] = [
{ sprite: 'wheat', key: 'WHEAT' },
Expand Down
Loading