
AI CEO Speedrun - Farming Simulator 25
Hướng dẫn
Speedrun Rules
Speedrun Rules
A structured speedrun format for Farming Simulator 25 where an AI acts as your CEO. The AI reads your farm state, analyses available contracts and market prices, and issues prioritised orders.
You execute. AI decides!
The format works with any LLM that accepts image and text input: Claude, ChatGPT, Grok, Gemini, or any other.
The dashboard is a standalone HTML file — no install, no account, no dependencies.
Open it in a browser on your second screen and you are ready to run.
The fun is in the collaboration. The AI CEO has to figure out optimal strategy from the information you give it — market prices, available contracts, what equipment you own, what month it is. You are the hands. It is the brain.
The speedrun ruleset
Setting | Value |
|---|---|
Starting capital | $100,000 |
Economic difficulty | Normal |
Real-time limit | 1–10 hours (3 hours is the standard) |
Time compression | Player controlled — 1x to 360x, sleep mechanic allowed |
Allowed | All in-game mechanics — hired workers, AI helpers, contracts, leasing, loans, forestry, vehicle flipping |
Not allowed | Cheat codes or mods |
Scoring | Cash in account at session end only |
Important: All assets — fields, equipment, vehicles, production facilities — must be sold before time expires, and loans must be repaid. Final score is cash on account!
Build time for liquidation into your strategy.
Time spent communicating with your AI CEO counts as game time. Efficient briefings win.
Download the Dashboard.html
Download the Dashboard.html
<!--
/fs25-ai-ceo/dashboard.html
FS25 AI CEO Dashboard — v21
Made by: Admiral OOM
Changes v21:
- Timer now tracks wall-clock time (Date.now) instead of setInterval ticks.
Fixes timer pausing when browser tab is hidden, system sleeps, or focus is lost.
- CEO output format: strict numbered task list only. No reasoning, no explanations.
Each line: task number, action, player-direct or worker, speed instruction if relevant.
- Land sales clarified as instant — no special ordering required.
- Leasing added as explicit supported mechanic (rent equipment per contract, no purchase).
- Loan strategy corrected: borrow only when a specific deployment target justifies it
within the remaining real-time window. Never borrow speculatively.
Loan flag: if holding loan with no active deployment, order repayment immediately.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FS25 AI CEO Dashboard</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--font-sans: system-ui, -apple-system, sans-serif;
--surface-0: #f1efe8;
--surface-1: #f8f7f3;
--surface-2: #ffffff;
--border: rgba(0,0,0,0.1);
--border-strong: rgba(0,0,0,0.18);
--border-accent: #378add;
--border-success: #3b6d11;
--border-warning: #854f0b;
--border-danger: #a32d2d;
--text-primary: #2c2c2a;
--text-secondary: #5f5e5a;
--text-muted: #888780;
--text-accent: #185fa5;
--text-success: #3b6d11;
--text-warning: #854f0b;
--text-danger: #a32d2d;
--bg-accent: #e6f1fb;
--bg-success: #eaf3de;
--bg-warning: #faeeda;
--bg-danger: #fcebeb;
--radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--surface-0: #1a1a18; --surface-1: #222220; --surface-2: #2c2c2a;
--border: rgba(255,255,255,0.1); --border-strong: rgba(255,255,255,0.18);
--text-primary: #f1efe8; --text-secondary: #b4b2a9; --text-muted: #888780;
--text-accent: #85b7eb; --text-success: #c0dd97; --text-warning: #fac775; --text-danger: #f09595;
--bg-accent: #0c447c; --bg-success: #27500a; --bg-warning: #633806; --bg-danger: #791f1f;
}
}
body { font-family: var(--font-sans); background: var(--surface-0); color: var(--text-primary); padding: 1.5rem; max-width: 760px; margin: 0 auto; }
svg.icon { width: 14px; height: 14px; display: inline-block; vertical-align: -2px; fill: currentColor; flex-shrink: 0; }
svg.icon-lg { width: 20px; height: 20px; }
svg.icon-xl { width: 28px; height: 28px; }
.header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 0.5px solid var(--border); gap: 16px; }
.header-left { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.logo { width: 32px; height: 32px; background: var(--bg-success); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; }
.title { font-size: 16px; font-weight: 500; }
.subtitle { font-size: 12px; color: var(--text-muted); margin-top: 1px; }
.config-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
.config-label { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.config-select { font-size: 12px; padding: 4px 8px; border: 0.5px solid var(--border-strong); border-radius: var(--radius); background: var(--surface-2); color: var(--text-primary); cursor: pointer; }
.config-select:focus { outline: none; border-color: var(--border-accent); }
.config-select:disabled { opacity: 0.5; cursor: not-allowed; }
.timer { font-size: 28px; font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }
.timer.running { color: var(--text-success); }
.timer.warning { color: var(--text-warning); }
.timer.danger { color: var(--text-danger); }
.timer-label { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
.timer-btns { display: flex; gap: 6px; margin-top: 6px; justify-content: flex-end; }
.tbtn { font-size: 12px; padding: 4px 10px; border: 0.5px solid var(--border-strong); border-radius: var(--radius); background: transparent; color: var(--text-primary); cursor: pointer; }
.tbtn:hover { background: var(--surface-1); }
.tbtn.start { border-color: var(--border-success); color: var(--text-success); font-weight: 500; }
.tbtn.start:hover { background: var(--bg-success); }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; margin-bottom: 1.25rem; }
.metric { background: var(--surface-1); border-radius: var(--radius); padding: 0.75rem 1rem; }
.metric-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; display: flex; align-items: center; gap: 5px; }
.metric-value { font-size: 18px; font-weight: 500; }
.metric-value.success { color: var(--text-success); }
.metric-value.accent { color: var(--text-accent); }
.metric-value.warning { color: var(--text-warning); }
.section { margin-bottom: 1.25rem; }
.section-title { font-size: 12px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
.section-title span { font-weight: 400; text-transform: none; letter-spacing: 0; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; overflow: hidden; }
.state-card { background: var(--surface-2); border: 0.5px solid var(--border); border-radius: 12px; padding: 0.75rem 1rem; min-width: 0; overflow: hidden; }
.state-card-title { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
.tag-list { display: flex; flex-direction: column; gap: 5px; min-height: 24px; }
.tag-row { display: flex; align-items: center; gap: 6px; background: var(--bg-accent); border-radius: var(--radius); padding: 4px 8px; font-size: 11px; color: var(--text-accent); min-width: 0; }
.tag-name { flex: 1; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tag-cost { font-size: 11px; opacity: 0.8; white-space: nowrap; flex-shrink: 0; }
.tag-remove { cursor: pointer; opacity: 0.5; font-size: 15px; line-height: 1; flex-shrink: 0; }
.tag-remove:hover { opacity: 1; }
.skill-list { display: flex; flex-wrap: wrap; gap: 5px; min-height: 24px; }
.skill-tag { font-size: 11px; padding: 3px 8px; border-radius: 20px; display: flex; align-items: center; gap: 4px; }
.skill-tag.strength { background: var(--bg-success); color: var(--text-success); }
.skill-tag.weakness { background: var(--bg-danger); color: var(--text-danger); }
.skill-tag .remove { cursor: pointer; opacity: 0.6; font-size: 14px; line-height: 1; }
.skill-tag .remove:hover { opacity: 1; }
.empty-tag { font-size: 12px; color: var(--text-muted); font-style: italic; }
.add-row { display: flex; gap: 5px; margin-top: 8px; align-items: center; min-width: 0; }
.add-row input[type=text] { flex: 1; min-width: 0; font-size: 12px; padding: 5px 7px; border: 0.5px solid var(--border); border-radius: var(--radius); background: var(--surface-1); color: var(--text-primary); }
.add-row input[type=number].cost-field { flex: 0 0 72px; width: 72px; min-width: 0; font-size: 12px; padding: 5px 5px; border: 0.5px solid var(--border); border-radius: var(--radius); background: var(--surface-1); color: var(--text-warning); }
.add-row input:focus { outline: none; border-color: var(--border-accent); }
.add-row button { flex-shrink: 0; font-size: 16px; line-height: 1; padding: 4px 8px; border: 0.5px solid var(--border-strong); border-radius: var(--radius); background: transparent; color: var(--text-primary); cursor: pointer; }
.add-row button:hover { background: var(--surface-1); }
.cost-label { font-size: 10px; color: var(--text-muted); margin-top: 3px; }
.finance-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.finance-field label { font-size: 11px; color: var(--text-muted); display: block; margin-bottom: 4px; }
.finance-field input { width: 100%; font-size: 14px; font-weight: 500; padding: 6px 10px; border: 0.5px solid var(--border); border-radius: var(--radius); background: var(--surface-2); color: var(--text-primary); }
.finance-field input.cash { color: var(--text-success); }
.finance-field input.loan { color: var(--text-warning); }
.finance-field input.equity { color: var(--text-accent); }
.finance-field input:focus { outline: none; border-color: var(--border-accent); }
.finance-hint { font-size: 10px; color: var(--text-muted); margin-top: 3px; }
.month-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-bottom: 10px; }
.month-btn { font-size: 12px; padding: 5px 0; border: 0.5px solid var(--border); border-radius: var(--radius); background: transparent; color: var(--text-secondary); cursor: pointer; text-align: center; }
.month-btn:hover { border-color: var(--border-accent); color: var(--text-accent); }
.month-btn.active { background: var(--bg-success); border-color: var(--border-success); color: var(--text-success); font-weight: 500; }
.presets-wrap { margin-bottom: 6px; }
.preset { font-size: 11px; padding: 3px 8px; border: 0.5px solid var(--border); border-radius: 20px; cursor: pointer; color: var(--text-secondary); background: transparent; margin-right: 4px; margin-bottom: 4px; }
.preset:hover { border-color: var(--border-accent); color: var(--text-accent); }
.drop-zone { border: 0.5px dashed var(--border-strong); border-radius: 12px; padding: 1.5rem; text-align: center; cursor: pointer; background: var(--surface-2); position: relative; }
.drop-zone.dragover { border-color: var(--border-accent); background: var(--bg-accent); }
.drop-zone.has-image { border-style: solid; border-color: var(--border-success); }
.drop-zone input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.drop-label { font-size: 13px; color: var(--text-secondary); margin-top: 8px; }
.drop-sub { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.preview-img { max-width: 100%; border-radius: 8px; margin-top: 10px; max-height: 180px; object-fit: contain; }
.clear-img { font-size: 11px; color: var(--text-muted); cursor: pointer; margin-top: 6px; display: inline-block; }
.clear-img:hover { color: var(--text-danger); }
.report-area { width: 100%; border: 0.5px solid var(--border); border-radius: 12px; padding: 0.75rem 1rem; font-size: 14px; font-family: var(--font-sans); color: var(--text-primary); background: var(--surface-2); resize: vertical; min-height: 70px; line-height: 1.5; }
.report-area:focus { outline: none; border-color: var(--border-accent); }
.send-btn { width: 100%; margin-top: 8px; padding: 10px; border: 0.5px solid var(--border-strong); border-radius: var(--radius); background: transparent; color: var(--text-primary); font-size: 14px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; }
.send-btn:hover { background: var(--surface-1); }
.send-btn:active { transform: scale(0.98); }
.liq-banner { background: var(--bg-danger); border: 0.5px solid var(--border-danger); border-radius: var(--radius); padding: 8px 12px; font-size: 13px; color: var(--text-danger); display: none; align-items: center; gap: 8px; margin-bottom: 1rem; font-weight: 500; }
.score-banner { background: var(--bg-success); border: 0.5px solid var(--border-success); border-radius: 12px; padding: 1rem 1.25rem; text-align: center; margin-bottom: 1rem; display: none; }
.score-num { font-size: 32px; font-weight: 500; color: var(--text-success); }
.score-label { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
.output-area { background: var(--surface-1); border: 0.5px solid var(--border); border-radius: 12px; padding: 1rem; font-size: 14px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; margin-top: 8px; display: none; }
.api-note { font-size: 11px; color: var(--text-muted); margin-top: 6px; text-align: center; }
.confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; align-items: center; justify-content: center; z-index: 100; }
.confirm-overlay.visible { display: flex; }
.confirm-box { background: var(--surface-2); border: 0.5px solid var(--border-strong); border-radius: 12px; padding: 1.5rem; max-width: 320px; width: 90%; text-align: center; }
.confirm-box p { font-size: 14px; color: var(--text-primary); margin-bottom: 1rem; line-height: 1.5; }
.confirm-actions { display: flex; gap: 8px; justify-content: center; }
.confirm-yes { font-size: 13px; padding: 6px 16px; border: 0.5px solid var(--border-danger); border-radius: var(--radius); background: transparent; color: var(--text-danger); cursor: pointer; font-weight: 500; }
.confirm-yes:hover { background: var(--bg-danger); }
.confirm-no { font-size: 13px; padding: 6px 16px; border: 0.5px solid var(--border-strong); border-radius: var(--radius); background: transparent; color: var(--text-primary); cursor: pointer; }
.confirm-no:hover { background: var(--surface-1); }
</style>
</head>
<body>
<div class="confirm-overlay" id="confirm-overlay">
<div class="confirm-box">
<p>Reset the timer? This will end your current run.</p>
<div class="confirm-actions">
<button class="confirm-yes" onclick="confirmReset()">Reset run</button>
<button class="confirm-no" onclick="cancelReset()">Keep going</button>
</div>
</div>
</div>
<div class="liq-banner" id="liq-banner">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 2L1 21h22L12 2zm0 3.5L21 20H3L12 5.5zM11 10v4h2v-4h-2zm0 6v2h2v-2h-2z"/></svg>
5 minutes left — sell everything now, repay all loans.
</div>
<div class="score-banner" id="score-banner">
<div class="score-label">Final score</div>
<div class="score-num" id="final-score">$0</div>
<div class="score-label">cash in account at session end</div>
</div>
<div class="header">
<div class="header-left">
<div class="logo">
<svg class="icon-lg" viewBox="0 0 24 24" style="color:var(--text-success)"><path d="M5 5h7l1 5H5V5zm-2 6h11l1 4H2l1-4zm14-6h2l2 10h-1a3 3 0 0 0-6 0H9a3 3 0 0 0-6 0H2v-2h1L5 4h8l2 1zM6 18a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm11 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>
</div>
<div>
<div class="title">FS25 AI CEO</div>
<div class="subtitle" id="header-subtitle">Speedrun</div>
</div>
</div>
<div>
<div class="config-row">
<span class="config-label">Map:</span>
<select class="config-select" id="map-select" onchange="onConfigChange()">
<option value="riverbend" selected>Riverbend Springs ★</option>
<option value="hutan">Hutan Pantai</option>
<option value="zielonka">Zielonka</option>
<option value="silverrun">Silverrun Forest</option>
<option value="kinlaig">Kinlaig</option>
<option value="custom">Custom / mod map</option>
</select>
<span class="config-label">Duration:</span>
<select class="config-select" id="duration-select" onchange="onConfigChange()">
<option value="1">1 hr</option>
<option value="2">2 hrs</option>
<option value="3" selected>3 hrs ★</option>
<option value="4">4 hrs</option>
<option value="5">5 hrs</option>
<option value="6">6 hrs</option>
<option value="7">7 hrs</option>
<option value="8">8 hrs</option>
<option value="9">9 hrs</option>
<option value="10">10 hrs</option>
</select>
</div>
<div style="text-align:right;">
<div class="timer" id="timer-display">3:00:00</div>
<div class="timer-label" id="timer-label">ready to start</div>
<div class="timer-btns">
<button class="tbtn start" id="timer-btn" onclick="startRun()">Start run</button>
<button class="tbtn" id="reset-btn" onclick="requestReset()" style="display:none;">Reset</button>
</div>
</div>
</div>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-label"><svg class="icon" viewBox="0 0 24 24"><path d="M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z"/></svg>Cash</div>
<div class="metric-value success" id="display-cash">$100,000</div>
</div>
<div class="metric">
<div class="metric-label"><svg class="icon" viewBox="0 0 24 24"><path d="M20 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/></svg>Loan</div>
<div class="metric-value warning" id="display-loan">$0</div>
</div>
<div class="metric">
<div class="metric-label"><svg class="icon" viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>Equity</div>
<div class="metric-value accent" id="display-equity">$0</div>
</div>
<div class="metric">
<div class="metric-label"><svg class="icon" viewBox="0 0 24 24"><path d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"/></svg>Month</div>
<div class="metric-value" id="display-month">Aug</div>
</div>
</div>
<div class="section">
<div class="section-title">Farm status</div>
<div class="finance-grid">
<div class="finance-field">
<label>Cash balance ($)</label>
<input type="number" class="cash" id="cash-input" value="100000" min="0" oninput="updateFinance()" />
</div>
<div class="finance-field">
<label>Outstanding loan ($)</label>
<input type="number" class="loan" id="loan-input" value="0" min="0" max="500000" step="5000" oninput="updateFinance()" />
<div class="finance-hint">Max $500k · $5k increments · ~$1,666/month on full $500k</div>
</div>
<div class="finance-field">
<label>Asset equity ($)</label>
<input type="number" class="equity" id="equity-input" value="0" min="0" readonly style="opacity:0.7;cursor:default;" />
<div class="finance-hint">Auto-calculated from field + equipment costs below</div>
</div>
</div>
<div class="month-grid" id="month-grid"></div>
<div class="two-col">
<div class="state-card">
<div class="state-card-title">
<svg class="icon" viewBox="0 0 24 24"><path d="M15 3l-6 2-6-2v18l6 2 6-2 6 2V5l-6-2zm-6 16.5L3 17V5.5l6 2v12zm6 2l-4-1.33V7.17L15 6v13.5zm6-.5l-4 1.33V8.83L21 7v14z"/></svg>
Fields & land owned
</div>
<div class="tag-list" id="fields-list"><span class="empty-tag">none</span></div>
<div class="add-row">
<input type="text" id="field-name-input" placeholder="e.g. Field 32" onkeydown="if(event.key==='Enter')addAsset('field')" />
<input type="number" class="cost-field" id="field-cost-input" placeholder="$ cost" min="0" onkeydown="if(event.key==='Enter')addAsset('field')" />
<button onclick="addAsset('field')">+</button>
</div>
<div class="cost-label">Cost is optional — helps CEO track equity</div>
</div>
<div class="state-card">
<div class="state-card-title">
<svg class="icon" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.6-.22l-2.39.96a6.97 6.97 0 0 0-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.6.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.6.22l2.39-.96c.5.37 1.04.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.57 1.62-.94l2.39.96c.22.08.47 0 .6-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
Equipment & production assets
</div>
<div class="tag-list" id="equip-list"><span class="empty-tag">none</span></div>
<div class="add-row">
<input type="text" id="equip-name-input" placeholder="e.g. Chainsaw, Sawmill" onkeydown="if(event.key==='Enter')addAsset('equip')" />
<input type="number" class="cost-field" id="equip-cost-input" placeholder="$ cost" min="0" onkeydown="if(event.key==='Enter')addAsset('equip')" />
<button onclick="addAsset('equip')">+</button>
</div>
<div class="cost-label">Cost is optional — helps CEO track equity</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Player profile</div>
<div class="two-col">
<div class="state-card">
<div class="state-card-title">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
Strengths
</div>
<div class="presets-wrap">
<button class="preset" onclick="addPreset('strength','Forestry & logging')">Forestry</button>
<button class="preset" onclick="addPreset('strength','Logistics')">Logistics</button>
<button class="preset" onclick="addPreset('strength','Animal farming')">Animals</button>
<button class="preset" onclick="addPreset('strength','Field work')">Field work</button>
<button class="preset" onclick="addPreset('strength','Harvesting')">Harvesting</button>
<button class="preset" onclick="addPreset('strength','Production chains')">Production</button>
</div>
<div class="skill-list" id="strength-list"><span class="empty-tag">none added</span></div>
<div class="add-row">
<input type="text" id="strength-input" placeholder="custom skill" onkeydown="if(event.key==='Enter')addSkill('strength')" />
<button onclick="addSkill('strength')">+</button>
</div>
</div>
<div class="state-card">
<div class="state-card-title">
<svg class="icon" viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
Weaknesses
</div>
<div class="presets-wrap">
<button class="preset" onclick="addPreset('weakness','Forestry & logging')">Forestry</button>
<button class="preset" onclick="addPreset('weakness','Logistics')">Logistics</button>
<button class="preset" onclick="addPreset('weakness','Animal farming')">Animals</button>
<button class="preset" onclick="addPreset('weakness','Field work')">Field work</button>
<button class="preset" onclick="addPreset('weakness','Harvesting')">Harvesting</button>
<button class="preset" onclick="addPreset('weakness','Production chains')">Production</button>
</div>
<div class="skill-list" id="weakness-list"><span class="empty-tag">none added</span></div>
<div class="add-row">
<input type="text" id="weakness-input" placeholder="custom weakness" onkeydown="if(event.key==='Enter')addSkill('weakness')" />
<button onclick="addSkill('weakness')">+</button>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Screenshot <span>— contracts, shop, market, prices, map or any game screen</span></div>
<div class="drop-zone" id="drop-zone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
<input type="file" accept="image/*" onchange="handleFileInput(event)" id="file-input" />
<div id="drop-placeholder">
<svg class="icon-xl" viewBox="0 0 24 24" style="color:var(--text-muted);margin:0 auto;display:block;"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
<div class="drop-label">Drop screenshot here or click to upload</div>
<div class="drop-sub">Paste works too — Ctrl+V anywhere on this page</div>
</div>
<img id="preview-img" class="preview-img" style="display:none;" alt="Screenshot preview" />
</div>
<div id="clear-row" style="display:none;text-align:center;margin-top:6px;">
<span class="clear-img" onclick="clearImage()">Clear screenshot ×</span>
</div>
</div>
<div class="section">
<div class="section-title">Situation report <span>— optional</span></div>
<textarea class="report-area" id="report-text" placeholder="What just happened, what you see, prices noticed, worker status, current time speed..."></textarea>
<button class="send-btn" onclick="sendReport()">
<svg class="icon" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
Brief the CEO
</button>
<div class="output-area" id="output-area"></div>
<p class="api-note" id="api-note"></p>
</div>
<script>
const MAP_CONTEXT = {
riverbend: {
name: 'Riverbend Springs', region: 'North America (Arkansas, USA)',
terrain: 'Large flat fields, wide roads, river crossing via ferry. Best map for large equipment.',
forestry: 'Moderate. Forested areas available. Straight roads make log transport efficient.',
crops: 'Corn, soybeans, cotton, wheat, canola, sunflowers, peas (field 44), green beans (field 64).',
sellPoints: 'Grain mill, farmer\'s market, paper mill, multiple sell stands. Ferry allows north-south transport.',
notes: '4 construction projects. Large flat fields suit wide harvester headers. Lot adjacent to vehicle shop is key for vehicle flipping.',
},
hutan: {
name: 'Hutan Pantai', region: 'East Asia (Indonesia-inspired coastal)',
terrain: 'Hilly, uneven. Elevated highways. Coastal town in south.',
forestry: 'Strong. "Hutan Pantai" means Coastal Forest — trees are a core map feature.',
crops: 'Rice (main specialty), standard crops. Water buffalo available.',
sellPoints: 'Coastal town markets, Temple Ground construction project.',
notes: 'Rice cultivation unique to this map. Lot adjacent to vehicle shop worth assessing for flip operations.',
},
zielonka: {
name: 'Zielonka', region: 'Central Europe (Poland-inspired)',
terrain: 'Flat but narrow roads. Over 100 plots, fragmented. Challenging for large equipment.',
forestry: 'Good. European mixed deciduous. Narrow roads favour chainsaw over harvesters.',
crops: 'Wheat, canola, potatoes, sugar beets. Narrow fields limit very wide headers.',
sellPoints: 'Carpentry (791m), Grain River Silo (890m), Sawmill (1070m), Paper Factory (1533m), Farmers Market (1434m), Grain Barge Terminal 01 (1729m), Goldcrest Valley (1827m), Warehouse (865m).',
notes: 'Most fields of any base map. Piano Factory unique production outlet. Chainsaw forestry well suited here.',
},
silverrun: {
name: 'Silverrun Forest', region: 'Mod map (remastered FS22 Platinum)',
terrain: 'Forest-dominated. Built for forestry. Iron selling also available.',
forestry: 'EXCEPTIONAL. Maximum tree density of any available map. All chainsaw work is player-only time.',
crops: 'Limited. Forestry is the primary income stream.',
sellPoints: 'Sawmill, iron sell points.',
notes: 'Sleep-to-grow is the dominant play on this map. Iron selling is a secondary unique income stream.',
},
kinlaig: {
name: 'Kinlaig', region: 'Scotland (Highlands Fishing Expansion DLC)',
terrain: 'Rugged windswept Scottish Highlands. Rocky valleys, thick grass.',
forestry: 'Moderate highland forestry. Chainsaw required — player time only.',
crops: 'Highland crops, onions, wheat. Fishing is a unique parallel income stream.',
sellPoints: 'Fishing sell points (unique), highland farm markets.',
notes: 'Fishing can run as a parallel AI-assisted operation. Highland cows featured.',
},
custom: {
name: 'Custom / mod map', region: 'Unknown',
terrain: 'Unknown — CEO builds map knowledge from screenshots and situation reports.',
forestry: 'Unknown — report tree density, chainsaw viability, sawmill availability.',
crops: 'Unknown — report available crops and sell points as discovered.',
sellPoints: 'Unknown — report as discovered. Note any lot adjacent to the vehicle shop.',
notes: 'No pre-loaded map knowledge. Share screenshots of map overview and shop inventory in early briefings.',
},
};
// ── State ──
const DEFAULT_HOURS = 3;
let durationSeconds = DEFAULT_HOURS * 3600;
let startTimestamp = null; // wall-clock ms when run started
let running = false;
let tickInterval = null;
let liqShown = false;
let scoreShown = false;
let screenshotBase64 = null;
const fields = []; // {name, cost}
const equip = []; // {name, cost}
const strengths = [];
const weaknesses= [];
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
let selectedMonth = 'Aug';
// ── Config ──
function onConfigChange() {
const h = parseInt(document.getElementById('duration-select').value);
const mapKey = document.getElementById('map-select').value;
const map = MAP_CONTEXT[mapKey];
durationSeconds = h * 3600;
liqShown = false; scoreShown = false;
document.getElementById('liq-banner').style.display = 'none';
document.getElementById('score-banner').style.display = 'none';
document.getElementById('header-subtitle').textContent = `${map.name} · ${h}h speedrun`;
if (!running) renderTimer(durationSeconds);
}
// ── Month ──
function buildMonthGrid() {
document.getElementById('month-grid').innerHTML = MONTHS.map(m =>
`<button class="month-btn${m===selectedMonth?' active':''}" onclick="selectMonth('${m}')">${m}</button>`
).join('');
}
function selectMonth(m) {
selectedMonth = m;
document.getElementById('display-month').textContent = m;
buildMonthGrid();
}
// ── Timer (wall-clock based) ──
function pad(n) { return String(n).padStart(2,'0'); }
function formatTime(s) {
s = Math.max(0, s);
return Math.floor(s/3600)+':'+pad(Math.floor((s%3600)/60))+':'+pad(s%60);
}
function remaining() {
if (!running || !startTimestamp) return durationSeconds;
const elapsed = (Date.now() - startTimestamp) / 1000;
return Math.max(0, durationSeconds - elapsed);
}
function renderTimer(rem) {
const el = document.getElementById('timer-display');
el.textContent = formatTime(rem);
if (!running) { el.className = 'timer'; return; }
el.className = 'timer' + (rem <= 300 ? ' danger' : rem <= 600 ? ' warning' : ' running');
}
function tick() {
const rem = remaining();
renderTimer(rem);
if (rem <= 300 && !liqShown) {
document.getElementById('liq-banner').style.display = 'flex';
liqShown = true;
}
if (rem <= 0 && !scoreShown) {
endRun();
}
}
function startRun() {
startTimestamp = Date.now();
running = true; liqShown = false; scoreShown = false;
document.getElementById('timer-btn').style.display = 'none';
document.getElementById('reset-btn').style.display = 'inline-block';
document.getElementById('duration-select').disabled = true;
document.getElementById('map-select').disabled = true;
document.getElementById('timer-label').textContent = 'in progress';
tickInterval = setInterval(tick, 500); // 500ms tick for responsiveness
tick();
}
function endRun() {
clearInterval(tickInterval); running = false; scoreShown = true;
const cash = parseInt(document.getElementById('cash-input').value)||0;
document.getElementById('final-score').textContent = '$' + cash.toLocaleString();
document.getElementById('score-banner').style.display = 'block';
document.getElementById('timer-label').textContent = "time's up";
document.getElementById('timer-display').className = 'timer danger';
document.getElementById('timer-display').textContent = '0:00:00';
}
function requestReset() { document.getElementById('confirm-overlay').classList.add('visible'); }
function cancelReset() { document.getElementById('confirm-overlay').classList.remove('visible'); }
function confirmReset() {
clearInterval(tickInterval); running = false; startTimestamp = null;
liqShown = false; scoreShown = false;
document.getElementById('liq-banner').style.display = 'none';
document.getElementById('score-banner').style.display = 'none';
document.getElementById('timer-btn').style.display = 'inline-block';
document.getElementById('reset-btn').style.display = 'none';
document.getElementById('duration-select').disabled = false;
document.getElementById('map-select').disabled = false;
document.getElementById('timer-label').textContent = 'ready to start';
document.getElementById('confirm-overlay').classList.remove('visible');
renderTimer(durationSeconds);
}
// ── Finance ──
function recalcEquity() {
const total = [...fields, ...equip].reduce((s, a) => s + (a.cost || 0), 0);
document.getElementById('equity-input').value = total;
document.getElementById('display-equity').textContent = '$' + total.toLocaleString();
}
function updateFinance() {
const cash = parseInt(document.getElementById('cash-input').value)||0;
const loan = parseInt(document.getElementById('loan-input').value)||0;
document.getElementById('display-cash').textContent = '$' + cash.toLocaleString();
document.getElementById('display-loan').textContent = '$' + loan.toLocaleString();
}
// ── Assets ──
function renderAssets(arr, listId) {
const el = document.getElementById(listId);
if (arr.length === 0) { el.innerHTML = '<span class="empty-tag">none</span>'; return; }
el.innerHTML = arr.map((item, i) =>
`<div class="tag-row">
<span class="tag-name">${item.name}</span>
${item.cost ? `<span class="tag-cost">$${item.cost.toLocaleString()}</span>` : ''}
<span class="tag-remove" onclick="removeAsset('${listId}',${i})">×</span>
</div>`
).join('');
}
function addAsset(type) {
const nameId = type === 'field' ? 'field-name-input' : 'equip-name-input';
const costId = type === 'field' ? 'field-cost-input' : 'equip-cost-input';
const listId = type === 'field' ? 'fields-list' : 'equip-list';
const arr = type === 'field' ? fields : equip;
const nameEl = document.getElementById(nameId);
const costEl = document.getElementById(costId);
const name = nameEl.value.trim(); if (!name) return;
arr.push({ name, cost: parseInt(costEl.value)||0 });
nameEl.value = ''; costEl.value = '';
renderAssets(arr, listId);
recalcEquity();
}
function removeAsset(listId, idx) {
const arr = listId === 'fields-list' ? fields : equip;
arr.splice(idx, 1);
renderAssets(arr, listId);
recalcEquity();
}
// ── Skills ──
function renderSkills(arr, listId, type) {
const el = document.getElementById(listId);
if (arr.length === 0) { el.innerHTML = '<span class="empty-tag">none added</span>'; return; }
el.innerHTML = arr.map((item, i) =>
`<span class="skill-tag ${type}">${item}<span class="remove" onclick="removeSkill('${listId}',${i})">×</span></span>`
).join('');
}
function addSkill(type) {
const input = document.getElementById(type + '-input');
const val = input.value.trim(); if (!val) return;
const arr = type === 'strength' ? strengths : weaknesses;
if (arr.includes(val)) return;
arr.push(val); input.value = '';
renderSkills(arr, type + '-list', type);
}
function addPreset(type, val) {
const arr = type === 'strength' ? strengths : weaknesses;
if (arr.includes(val)) return;
arr.push(val);
renderSkills(arr, type + '-list', type);
}
function removeSkill(listId, idx) {
const type = listId === 'strength-list' ? 'strength' : 'weakness';
const arr = type === 'strength' ? strengths : weaknesses;
arr.splice(idx, 1);
renderSkills(arr, listId, type);
}
// ── Image ──
function loadImage(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
screenshotBase64 = e.target.result;
const img = document.getElementById('preview-img');
img.src = screenshotBase64; img.style.display = 'block';
document.getElementById('drop-placeholder').style.display = 'none';
document.getElementById('drop-zone').classList.add('has-image');
document.getElementById('clear-row').style.display = 'block';
};
reader.readAsDataURL(file);
}
function clearImage() {
screenshotBase64 = null;
document.getElementById('preview-img').src = '';
document.getElementById('preview-img').style.display = 'none';
document.getElementById('drop-placeholder').style.display = 'block';
document.getElementById('drop-zone').classList.remove('has-image');
document.getElementById('clear-row').style.display = 'none';
document.getElementById('file-input').value = '';
}
function handleDragOver(e) { e.preventDefault(); document.getElementById('drop-zone').classList.add('dragover'); }
function handleDragLeave() { document.getElementById('drop-zone').classList.remove('dragover'); }
function handleDrop(e) { e.preventDefault(); document.getElementById('drop-zone').classList.remove('dragover'); loadImage(e.dataTransfer.files[0]); }
function handleFileInput(e) { loadImage(e.target.files[0]); }
document.addEventListener('paste', e => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) { if (item.type.startsWith('image/')) { loadImage(item.getAsFile()); break; } }
});
// ── Prompt ──
function buildPrompt() {
const cash = parseInt(document.getElementById('cash-input').value)||0;
const loan = parseInt(document.getElementById('loan-input').value)||0;
const equity = parseInt(document.getElementById('equity-input').value)||0;
const report = document.getElementById('report-text').value.trim();
const hours = document.getElementById('duration-select').value;
const mapKey = document.getElementById('map-select').value;
const map = MAP_CONTEXT[mapKey];
const rem = remaining();
const fieldLines = fields.length
? fields.map(f => f.cost ? `${f.name} (paid $${f.cost.toLocaleString()})` : f.name).join(', ')
: 'none';
const equipLines = equip.length
? equip.map(e => e.cost ? `${e.name} (paid $${e.cost.toLocaleString()})` : e.name).join(', ')
: 'none';
const profileBlock = [
strengths.length ? `Player strengths: ${strengths.join(', ')}` : '',
weaknesses.length ? `Player weaknesses: ${weaknesses.join(', ')}` : '',
].filter(Boolean).join('\n') || 'Player profile: not set';
const systemContext = `You are the CEO of a Farming Simulator 25 operation in a real-time speedrun. Maximise liquid cash at session end. The player executes your orders exactly — they do not decide strategy.
OUTPUT FORMAT — STRICT:
Respond only with a numbered task list. No explanations, no reasoning, no preamble.
Each line: [number]. [Action] — [PLAYER] or [WORKER] — [speed/sleep instruction if relevant]
Example:
1. Sell Field 32 — PLAYER — instant
2. Return combine to shop — WORKER — 15x speed
3. Sleep to January — PLAYER — set 360x then sleep
Maximum 8 tasks. If a task needs context, put it in brackets after the action. Stop when the list is complete.
MAP: ${map.name} (${map.region})
Terrain: ${map.terrain}
Forestry: ${map.forestry}
Crops: ${map.crops}
Sell points: ${map.sellPoints}
Notes: ${map.notes}
GAME MECHANICS:
- Land sells instantly. No special order needed — sell at any time.
- Leasing: equipment can be leased per contract without purchasing. Use leasing when a contract needs equipment you do not own and purchase is not justified by remaining time.
- Chainsaw/forestry: PLAYER only — cannot be delegated to workers.
- AI workers: field contracts, tractor ops, seeding, cultivation, fertilising, wide-header harvesting, transport.
- Time compression: 1x to 360x. Sleep-to-next-month skips time instantly.
- Sleep plays: tree growth cycles, price timing (hold produce → sleep to peak month → sell), season skipping with workers assigned.
LOAN RULES — CRITICAL:
- Max $500k, $5k increments, ~$1,666/month interest on full amount.
- NEVER borrow speculatively. Only take a loan when: (a) you have a specific asset or deployment in mind, (b) the expected return exceeds the interest cost within remaining real time, AND (c) there is enough time to repay before session end.
- Holding a loan with no active deployment plan = repay immediately.
- Loans block time-skip mechanics if interest becomes a significant drain. Factor this in.
- Repay all loans before session end. Final score = cash only.
PRICE INTELLIGENCE (confirmed — Zielonka):
Wood (raw logs) : HIGH Mar (~1800) LOW Sep/Oct (~1703) → sell spring
Wood Beam : PEAK Sep (~5269) crashes Oct → sell September only
Wood Chips : LOW Jul (~306) PEAK Jan (~973) nearly 3x → hold for winter
Wheat : LOW Aug (~497) PEAK Jan (~734) → sleep to winter if stored
Clothes : PEAK May (~19737) LOW Oct (~11980) → spring window
Carrots : minor swing, very slow harvest → avoid contracts
HARVESTER WIDTH FILTER:
Wide/fast (9-14m): wheat, barley, canola, oats → prioritise
Medium (6-9m): corn, sunflowers
Slow (3-4m): potatoes, sugar beets
Very slow (2m): carrots, root veg (~7x longer than wheat per hectare) → reject unless exceptional
Extremely slow: grapes, olives → reject
INCOME STREAMS:
Forestry: chainsaw→logs→sawmill→planks→carpentry→furniture. Sleep-to-grow for tree cycles. Player only.
Vehicle flipping: buy used, repair, repaint, resell. Lot adjacent to vehicle shop = no transport time.
Production chains: bakery, woodworking, stone/tile factory. Only if player has Production chains strength.
Contracts: use for early cash. Workers on contracts free the player. Wide-header crops only.
Leasing: use when a high-value contract needs equipment not worth buying for remaining session time.
LIQUIDATION:
At 5 minutes remaining: sell everything. Land sells instantly. Repay all loans.
Plan backwards — do not make purchases in the final 10 minutes unless they can be sold again in time.
PLAYER ROUTING:
Route tasks to match player strengths. Never idle the player.
If Logistics strength: prioritise long-haul and multi-stop sell runs.
If Production chains strength: factory setup is viable mid-game.
If Production chains weakness: raw material sales only, no factory chains.`;
return [
systemContext,
'',
'[FS25 CEO BRIEFING]',
`Real time remaining: ${formatTime(rem)} of ${hours}h | Month: ${selectedMonth} | Map: ${map.name}`,
`Cash: $${cash.toLocaleString()} | Loan: $${loan.toLocaleString()} | Equity (paid costs): $${equity.toLocaleString()}`,
`Fields: ${fieldLines}`,
`Equipment: ${equipLines}`,
profileBlock,
report ? `Situation: ${report}` : '',
screenshotBase64 ? 'Screenshot attached.' : 'No screenshot.',
'',
'Give me the task list.',
].filter(Boolean).join('\n');
}
// ── Send ──
async function sendReport() {
const prompt = buildPrompt();
document.getElementById('report-text').value = '';
if (typeof sendPrompt === 'function') {
sendPrompt(prompt);
return;
}
const output = document.getElementById('output-area');
const note = document.getElementById('api-note');
output.style.display = 'block';
output.textContent = 'Briefing CEO...';
note.textContent = '';
const imageBlock = screenshotBase64 ? [{
type: 'image',
source: { type: 'base64', media_type: 'image/png', data: screenshotBase64.split(',')[1] }
}] : [];
try {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 400,
messages: [{ role: 'user', content: [...imageBlock, { type: 'text', text: prompt }] }]
})
});
const data = await res.json();
output.textContent = data.content?.map(b => b.text||'').join('') || JSON.stringify(data);
} catch (err) {
output.textContent = 'Error: ' + err.message;
note.textContent = 'Requires a server environment with API key configured.';
}
}
// ── Init ──
buildMonthGrid();
onConfigChange();
updateFinance();
</script>
</body>
</html>Map and Duration
Map and Duration
Before hitting "Start Run", select which Map you are working on and Duration of your speedrun.
Then fill in your player profile with strength and weaknesses.
First briefing
First briefing
When you load your FS25 save and are ready:
- Set August as your starting month (default)
- Hit Start Run on the dashboard — timer begins immediately
- Navigate to the contracts or prices screen in FS25 (optional if you love doing contracts)
- Take a screenshot and paste or drop it into the dashboard (Ctrl+V works)
- Add any context in the situation report field
- Hit Brief the CEO
- Copy the generated briefing into your LLM
- Execute the orders you receive
Liquidation and final Score!
Liquidation and final Score!
The dashboard warns you at 5 minutes remaining. When the warning appears:
- Stop taking new contracts or making purchases
- Begin selling all equipment and vehicles
- Sell production assets (sawmill, workshop, etc.)
- Sell all owned land
- Repay all outstanding loans
When the timer hits zero, enter your final cash balance. That is your score.
Thank you Fizbee - the original idea
Thank you Fizbee - the original idea
Related blueprints
Other builds that share materials, tools, or techniques with this one.






CC0 Phạm vi công cộng
Bản thiết kế này được phát hành theo CC0. Bạn tự do sao chép, sửa đổi, phân phối và sử dụng cho bất kỳ mục đích nào mà không cần xin phép.
Hỗ trợ nhà sáng tạo bằng cách mua sản phẩm qua bản thiết kế, nơi họ nhận Hoa hồng nhà sáng tạo do nhà bán hàng đặt, hoặc tạo phiên bản mới và kết nối trong bản thiết kế riêng để chia sẻ doanh thu.
