Compare commits
3 Commits
d570425402
...
feat/overs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a4fe59811 | ||
|
|
3a65bf5d59 | ||
|
|
8e1e313b20 |
@@ -349,7 +349,28 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen flex-col bg-base-200">
|
<div class="flex h-screen flex-col bg-base-200">
|
||||||
<header class="flex items-center gap-3 border-b border-base-300 bg-base-100 px-4 py-2">
|
<header class="flex items-center gap-3 border-b border-base-300 bg-base-100 px-4 py-2">
|
||||||
<img src="/favicon.svg" alt="" class="size-6" />
|
<!-- Inlined favicon so the logo strokes with the theme primary (currentColor
|
||||||
|
via text-primary); an external <img> can't read the theme var. -->
|
||||||
|
<svg
|
||||||
|
class="size-6 text-primary"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M14 20a2 2 0 1 0 -4 0a2 2 0 0 0 4 0" />
|
||||||
|
<path d="M14 4a2 2 0 1 0 -4 0a2 2 0 0 0 4 0" />
|
||||||
|
<path d="M6 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0" />
|
||||||
|
<path d="M22 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0" />
|
||||||
|
<path d="M14 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0" />
|
||||||
|
<path d="M6 12h4" />
|
||||||
|
<path d="M14 12h4" />
|
||||||
|
<path d="M12 6v4" />
|
||||||
|
<path d="M12 14v4" />
|
||||||
|
</svg>
|
||||||
<h1 class="text-lg font-semibold">meadows</h1>
|
<h1 class="text-lg font-semibold">meadows</h1>
|
||||||
<span class="text-sm text-base-content/50">
|
<span class="text-sm text-base-content/50">
|
||||||
{{ store.nodeCount }} {{ store.nodeCount === 1 ? "element" : "elements" }}
|
{{ store.nodeCount }} {{ store.nodeCount === 1 ? "element" : "elements" }}
|
||||||
|
|||||||
@@ -44,11 +44,17 @@ const fill = computed(() => sim.fill(props.id))
|
|||||||
<Handle :id="HANDLE_IN" type="target" :position="Position.Left" />
|
<Handle :id="HANDLE_IN" type="target" :position="Position.Left" />
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<NodeLabel :node-id="props.id" :name="stock.name" />
|
<NodeLabel :node-id="props.id" :name="stock.name" />
|
||||||
<div v-if="value !== null" class="mt-0.5 font-mono text-xs tabular-nums text-base-content/70">
|
</div>
|
||||||
|
<Handle :id="HANDLE_OUT" type="source" :position="Position.Right" />
|
||||||
|
<!-- Live value sits *below* the box as an out-of-flow overlay, so the box keeps
|
||||||
|
its exact size and the handles — with the pipes and links on them — never
|
||||||
|
shift when a run is engaged. -->
|
||||||
|
<div
|
||||||
|
v-if="value !== null"
|
||||||
|
class="pointer-events-none absolute inset-x-0 top-full mt-1 whitespace-nowrap text-center font-mono text-xs tabular-nums text-base-content/70"
|
||||||
|
>
|
||||||
{{ formatValue(value)
|
{{ formatValue(value)
|
||||||
}}<span v-if="stock.unit" class="ml-0.5 text-base-content/50">{{ stock.unit }}</span>
|
}}<span v-if="stock.unit" class="ml-0.5 text-base-content/50">{{ stock.unit }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Handle :id="HANDLE_OUT" type="source" :position="Position.Right" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,6 +33,14 @@
|
|||||||
* 11. Drift to low performance — a goal that erodes toward actual performance, so a
|
* 11. Drift to low performance — a goal that erodes toward actual performance, so a
|
||||||
* Reinforcing loop ratchets both downward.
|
* Reinforcing loop ratchets both downward.
|
||||||
*
|
*
|
||||||
|
* Last, the dynamic the book is named for, and the one the gallery has saved until a
|
||||||
|
* reader knows every piece it needs:
|
||||||
|
*
|
||||||
|
* 12. Overshoot and collapse — a Reinforcing engine running on a *non-renewable*
|
||||||
|
* Stock (the first with no inflow): it overshoots the
|
||||||
|
* limit instead of settling at it, the dark twin of
|
||||||
|
* "Limits to growth" — the ceiling erodes, so it crashes.
|
||||||
|
*
|
||||||
* These are plain data built from the same tested constructors the store uses
|
* These are plain data built from the same tested constructors the store uses
|
||||||
* (factory.ts), so every sample is a valid Model by construction. `build()`
|
* (factory.ts), so every sample is a valid Model by construction. `build()`
|
||||||
* mints fresh ids on each call, so loading a sample twice never collides.
|
* mints fresh ids on each call, so loading a sample twice never collides.
|
||||||
@@ -571,6 +579,68 @@ function driftToLowPerformance(): Model {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overshoot and collapse — the dark twin of "Limits to growth". The same
|
||||||
|
* Reinforcing engine runs, but the limit here is a *non-renewable* Resource that
|
||||||
|
* only depletes: a Stock with no inflow, the first in the gallery. An economy
|
||||||
|
* (Capital) lives off it — extraction grows with both the Resource left and the
|
||||||
|
* Capital deployed (Resource, Capital → [+] → extraction), and the revenue is
|
||||||
|
* reinvested as new Capital (extraction → [+] → investment → Capital), so the loop
|
||||||
|
* Capital → extraction → investment → Capital carries no `−` → Reinforcing. Capital
|
||||||
|
* climbs and extraction accelerates, but every unit burned is gone for good, so the
|
||||||
|
* Resource crosses the break-even level, the engine starves, and depreciation
|
||||||
|
* (Capital → [+] → depreciation, a Balancing drain) takes Capital down: it peaks,
|
||||||
|
* then collapses. Contrast "Predator and prey", whose prey regrows and so settles
|
||||||
|
* into oscillation — a finite Resource cannot, so it overshoots and crashes instead.
|
||||||
|
*/
|
||||||
|
function overshootAndCollapse(): Model {
|
||||||
|
// Resource on the left drains only downward (no inflow). Capital on the right runs
|
||||||
|
// a full Source → investment → Capital → depreciation → Sink column. The two
|
||||||
|
// coupling links — Capital → extraction and extraction → investment — cross in the
|
||||||
|
// open centre, where the R badge lands.
|
||||||
|
const resource = makeStock({ x: -240, y: 0 }, "Resource")
|
||||||
|
resource.initialValue = 1000
|
||||||
|
const extractionSink = makeCloud({ x: -240, y: 360 })
|
||||||
|
const extraction = makeFlow({ x: -240, y: 160 }, "extraction", resource.id, extractionSink.id)
|
||||||
|
// extraction = factor × Resource × Capital (both `+`): more capital extracts
|
||||||
|
// faster, scarcer resource slower. The bilinear term the non-negative floor tames.
|
||||||
|
extraction.rule = { kind: "proportional", factor: 0.0004 }
|
||||||
|
const capital = makeStock({ x: 240, y: 0 }, "Capital")
|
||||||
|
capital.initialValue = 5
|
||||||
|
const investmentSource = makeCloud({ x: 240, y: -360 })
|
||||||
|
const investment = makeFlow({ x: 240, y: -160 }, "investment", investmentSource.id, capital.id)
|
||||||
|
// investment = factor × extraction (its one `+` input): the revenue reinvested —
|
||||||
|
// a Flow feeding a Flow, the edge that closes the Reinforcing loop through Capital.
|
||||||
|
investment.rule = { kind: "proportional", factor: 0.5 }
|
||||||
|
const depreciationSink = makeCloud({ x: 240, y: 360 })
|
||||||
|
const depreciation = makeFlow({ x: 240, y: 160 }, "depreciation", capital.id, depreciationSink.id)
|
||||||
|
// depreciation = factor × Capital (its `+` input): the Balancing drain that wins
|
||||||
|
// once the Resource can no longer feed investment.
|
||||||
|
depreciation.rule = { kind: "proportional", factor: 0.04 }
|
||||||
|
return model(
|
||||||
|
"Overshoot and collapse",
|
||||||
|
[
|
||||||
|
resource,
|
||||||
|
extractionSink,
|
||||||
|
extraction,
|
||||||
|
capital,
|
||||||
|
investmentSource,
|
||||||
|
investment,
|
||||||
|
depreciationSink,
|
||||||
|
depreciation,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
link(resource, extraction, "+"),
|
||||||
|
link(capital, extraction, "+"),
|
||||||
|
link(extraction, investment, "+"),
|
||||||
|
link(capital, depreciation, "+"),
|
||||||
|
],
|
||||||
|
// Capital starts at 5, overshoots to ~250 by t≈39, and collapses back near its
|
||||||
|
// starting level by t=150 — the full boom-and-bust arc, no dead tail.
|
||||||
|
{ start: 0, stop: 150, dt: 1 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** The gallery, ordered simplest first. */
|
/** The gallery, ordered simplest first. */
|
||||||
export const SAMPLES: Sample[] = [
|
export const SAMPLES: Sample[] = [
|
||||||
{ title: "Bathtub", blurb: "A stock filled and drained — no feedback yet.", build: bathtub },
|
{ title: "Bathtub", blurb: "A stock filled and drained — no feedback yet.", build: bathtub },
|
||||||
@@ -624,4 +694,9 @@ export const SAMPLES: Sample[] = [
|
|||||||
blurb: "Goals erode toward actual: a Reinforcing slide downhill.",
|
blurb: "Goals erode toward actual: a Reinforcing slide downhill.",
|
||||||
build: driftToLowPerformance,
|
build: driftToLowPerformance,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Overshoot and collapse",
|
||||||
|
blurb: "A growth engine burns a finite Resource: it peaks, then crashes.",
|
||||||
|
build: overshootAndCollapse,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user