Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i

- **Other maps.** Turn the whole building into a castle: sit on a throne of iron blades while your workers line up before you when they're done, send new ones off through the Hand of the King, and watch their beards grow long and grey as they toil. Send one home and the Kingsguard runs up from the dungeon, marches it down the stairs and throws it in a cell, where it starves, dies and rots down to a skeleton. Or into a space station in orbit, the Earth turning outside its windows: you run it from the captain's chair on the bridge, and a worker sent home is marched to the airlock and blown out into space, to drift off past the observation windows with everyone who went before it. Or make a map of your own, with its own way of seeing workers off in JSON ([docs/maps.md](docs/maps.md)).

There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md).
There's a lot more (a rooftop bar, an office dog, an arcade, a chess corner behind the couch, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md).

## Requirements

Expand Down
2 changes: 1 addition & 1 deletion docs/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Back to the [README](../README.md).
| W A S D / arrows | Walk (hold Shift to run); on the ladder, W and S climb; in a car, W is the gas, S brakes and reverses, A and D steer |
| Space | Jump (you can land on desks, couches and the cars in the garage); in a car, brake |
| Mouse drag / wheel | Orbit / zoom the camera |
| E | Interact: hire a worker, open its terminal, read a board, take an issue's note off the board, prompt a board agent, call a meeting in the meeting room, draw on the whiteboard, read the docs at the bookshelf, watch the TV, sit down (or get up), ride the elevator, climb the ladder (or get off it), slide down a fire pole, grab a coffee, take a smoke break, tee off at the golf tee, pet the dog, pick up the basketball (then hold E and let go to shoot), order a drink at the rooftop bar, blow the DJ's air horn, step up to the dart board or the axe lane on the roof (then hold Space and let go to throw), get into one of the cars in the garage (behind the wheel, or beside whoever's driving) or out of it, knock through the north wall past the gong for 2 more desks (at the **🚧 Room to grow** sign). In the [castle](maps.md#the-castle): sit on the throne, where E is for whoever's first in line (or the Hand of the King, with nobody waiting), and speak to the Hand to send out a new worker. On the [space station](maps.md#the-space-station) it's the captain's chair and the First Officer |
| E | Interact: hire a worker, open its terminal, read a board, take an issue's note off the board, prompt a board agent, call a meeting in the meeting room, draw on the whiteboard, read the docs at the bookshelf, watch the TV, sit down (or get up), play chess from a chess chair behind the couch, ride the elevator, climb the ladder (or get off it), slide down a fire pole, grab a coffee, take a smoke break, tee off at the golf tee, pet the dog, pick up the basketball (then hold E and let go to shoot), order a drink at the rooftop bar, blow the DJ's air horn, step up to the dart board or the axe lane on the roof (then hold Space and let go to throw), get into one of the cars in the garage (behind the wheel, or beside whoever's driving) or out of it, knock through the north wall past the gong for 2 more desks (at the **🚧 Room to grow** sign). In the [castle](maps.md#the-castle): sit on the throne, where E is for whoever's first in line (or the Hand of the King, with nobody waiting), and speak to the Hand to send out a new worker. On the [space station](maps.md#the-space-station) it's the captain's chair and the First Officer |
| K | On the castle's throne (or in the station's captain's chair): speak to the Hand of the King (the First Officer), to send out a new worker |
| P | Prompt: give a task to a new worker, or to the one at this desk |
| C | Changes: the files the worker at this desk changed and their diff; commit, discard or open a PR |
Expand Down
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Everything in the office, room by room. Back to the [README](../README.md).
- **Golf off the balcony.** Next to the ashtray there's a tee: a square of turf, a ball on a tee and a bag of clubs. Across the street, where the neighbours leave a gap, a fairway runs up to a green with a flag on it, 43 m out and a storey down (further down from the floors above). Press **E** at the tee to step up with a club. The camera drops down behind the ball. The mouse or **A**/**D** aims, **W**/**S** set the loft (too flat and it hits the railing), and holding **Space** takes the club back while the power meter runs up and down: let go to hit. The camera follows the ball out and down, and a label says where it stopped and how far from the pin. Everyone on your floor sees you swing and your ball fly, landing in the same spot. Hole one and the green goes up in confetti. Your closest shot and your holes in one are kept in your browser. **E** puts the club back.
- **Take a seat.** Press **E** at the lounge couch, a beanbag, the balcony bench or a stool at its bistro table, or the couch or the boss's chair up in the boss office to sit down. Walk off, jump or press **E** again to get up. Everyone on your floor sees you sitting. Sit on the couch facing the TV while someone's sharing their screen and it opens full screen for you.
- **Minesweeper on the boss's monitor.** In the boss's chair, **E** plays Minesweeper on the monitor: the camera moves in close and the board sits right on the screen. Click to dig, right-click (or Shift-click) to flag, and click a number to dig around it once its mines are flagged. The first click is never a mine. **Stop playing** leaves your game up on the monitor, with its clock paused, until you come back. Walk off to get up.
- **Chess behind the couch.** Behind the lounge couch (its back to the room) there's a little chess corner: a modern table with two chairs facing each other and a teal-and-foam board on it. Press **E** on a chair to sit down, and **E** again to play chess: pick one of your idle agents as your opponent (chair 1 plays White, chair 2 Black) or play both sides yourself. The window shows whose turn it is, what each side has taken and the moves so far, with **New game** and **Resign**. Pawns choose what they become, and checkmate, stalemate and draws all end the game. Walk off to get up.
- **Pick your character.** The first time you join, a character select screen lets you choose your name, skin tone, hair style, hair color and shirt, with a spinning preview. No name in mind? Leave it blank (or skip the screen) and you go by the made-up one shown in the box, like *Sunny Otter*, or press **🎲** for another. The office remembers your name and look in that browser, so you only pick once. Change it any time from **⚙️** or by clicking your name under *In the office* (turn on **👥 People** in the **☰** menu).
- **Hire workers.** Walk up to an empty desk and press **E** to choose Claude Code, OpenCode, Codex, Grok, Muse, DeepSeek Harness, Pi or Cursor, or press **P** to write a task first. A little worker sits down, a laptop opens, and the agent's live screen appears on it. Choose the agent for each queue task too.
- **Signs over the desks.** Press **L** at any desk, empty or not, to hang a big sign from the ceiling over it: *Operations*, *Code cleanup*, *Frontend*, whatever that desk is for. Type up to 32 characters (or pick one of the ideas), choose one of seven colors and press **Enter**. It hangs over the far edge of the desk, facing the chair, so it reads over the shoulder of whoever sits there; while the desk back to back with it has no sign of its own, it says the same on the back, so you can read it from across the room either way. Everyone on the floor sees it, the hint at the desk names it, and it stays up across restarts. **L** again changes it or takes it down. Each floor has its own.
Expand Down
3 changes: 2 additions & 1 deletion src/client/core/maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { builtFloors } from './floors';
import type { Parts } from './parts';
import { streetOf } from './worlds';

export type MapsParts = Pick<Parts, 'stage' | 'worlds' | 'place' | 'travel' | 'arrival' | 'views' | 'walking' | 'peers' | 'telescope' | 'smoking' | 'hoops' | 'arcade' | 'cabinet' | 'dog' | 'jukebox' | 'boards'>;
export type MapsParts = Pick<Parts, 'stage' | 'worlds' | 'place' | 'travel' | 'arrival' | 'views' | 'walking' | 'peers' | 'telescope' | 'smoking' | 'hoops' | 'arcade' | 'chess' | 'cabinet' | 'dog' | 'jukebox' | 'boards'>;

/** Registers the floor's paint (store 'floors'), the map (store 'map') and the floors' waiting count (the 'floors' message). */
export function installMaps(ctx: Ctx, core: CoreState, parts: MapsParts) {
Expand Down Expand Up @@ -62,6 +62,7 @@ export function installMaps(ctx: Ctx, core: CoreState, parts: MapsParts) {
hands.holdBall(false);
for (const r of parts.peers.remotes.values()) r.person.holdBall(false);
parts.arcade.stop();
parts.chess.stop();
parts.cabinet.stop();
worlds.world().group.visible = false;
worlds.enter(next);
Expand Down
2 changes: 2 additions & 0 deletions src/client/core/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { installBookshelf } from '../features/bookshelf';
import type { installCabinet } from '../features/cabinet';
import type { installCarrying } from '../features/carrying';
import type { installCars } from '../features/cars';
import type { installChess } from '../features/chess';
import type { installClimbing } from '../features/climbing';
import type { installCoffee } from '../features/coffee';
import type { installDog } from '../features/dog';
Expand Down Expand Up @@ -97,6 +98,7 @@ export interface Parts {
gallery: Made<typeof installGallery>;
tv: Made<typeof installTv>;
arcade: Made<typeof installArcade>;
chess: Made<typeof installChess>;
rooftop: Made<typeof installRooftop>;
telescope: Made<typeof installTelescope>;
dog: Made<typeof installDog>;
Expand Down
84 changes: 84 additions & 0 deletions src/client/features/chess/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// The idle agents' moves: a greedy 1-ply that takes what's free, keeps its own pieces, likes the
// middle and never misses a mate in one. Enough for a lunch-break game in the lounge.

import { ChessGame, file, rank, type Move, type PieceType } from './engine';
import { store } from '../../state';

const VALUE = { p: 100, n: 320, b: 330, r: 500, q: 900, k: 0 } as const;
const MATE = 100_000;

/** Shuffles in place with `rng`, so equally good moves don't always come out the same way. */
function shuffle<T>(xs: T[], rng: () => number): T[] {
for (let i = xs.length - 1; i > 0; i--) {
const j = (rng() * (i + 1)) | 0;
[xs[i], xs[j]] = [xs[j], xs[i]];
}
return xs;
}

/** How the board looks for `me` once it has moved: material, the middle, and whether it's over. */
function score(g: ChessGame, me: 'w' | 'b'): number {
const mat = material(g, me);
const status = g.status();
if (status.over) {
if (status.reason === 'checkmate') return status.winner === me ? MATE + g.history.length : -MATE;
// Winning and settling for a draw is throwing the game away.
return mat > 200 ? -50_000 : 0;
}
// Material counted once above; here only the pull toward the middle.
let n = mat;
for (let sq = 0; sq < 64; sq++) {
const p = g.pieces[sq];
if (!p) continue;
const v = center(p.t, sq);
n += p.c === me ? v : -v;
}
// A check is worth a nudge: most checks here come with something behind them. (Off the end of
// the game the opponent is the side to move; `winner` is only set on checkmate, above.)
if (status.check && g.turn !== me) n += 30;
return n;
}

/** A nudge toward the middle for the little pieces, so games don't all hug the edges. */
function center(t: PieceType, sq: number): number {
if (t !== 'p' && t !== 'n') return 0;
const d = Math.abs(3.5 - file(sq)) + Math.abs(3.5 - rank(sq));
return (t === 'p' ? 6 : 10) * (7 - d);
}

function material(g: ChessGame, me: 'w' | 'b'): number {
let n = 0;
for (const p of g.pieces) {
if (!p) continue;
n += p.c === me ? VALUE[p.t] : -VALUE[p.t];
}
return n;
}

/** An idle agent (waiting for a first prompt) the chess window offers as an opponent, by name. */
export function idleOpponents(): { id: string; name: string }[] {
return [...store.workers.values()]
.filter((w) => w.status === 'idle' && w.kind === 'agent')
.map((w) => ({ id: w.id, name: w.name }))
.sort((a, b) => a.name.localeCompare(b.name));
}

/** The agent's move in `game` (it must be its turn), or undefined when the game is over. */
export function chooseMove(game: ChessGame, rng: () => number = Math.random): Move | undefined {
const me = game.turn;
const moves = shuffle(game.allMoves(), rng);
if (!moves.length) return undefined;
let best: Move | undefined;
let bestScore = -Infinity;
for (const m of moves) {
const next = game.clone();
next.play({ from: m.from, to: m.to, promo: m.promo ?? 'q' });
// It always queens in the tactics below; queening for real is whoever's idea it was.
const s = score(next, me) + rng() * 8;
if (s > bestScore) {
bestScore = s;
best = m;
}
}
return best;
}
Loading