Nimbin[12]?Games / Entropie / commits / a8e8d7c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (17 hours ago) · synced 8 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz

entropie 2.5.0: cigarette butts go somewhere

Christian Immanuel · 2026-10-20 05:59 · a8e8d7cc725e7a65c01b07299bcdeee784825b19

- Finished cigarettes go into the fire when close, else the tin or the bin
  when near; tidy people walk there with the butt
- Pocket ashtrays (you and Jana): the butt goes in there
- No bin near: the tidy ask for a pocket ashtray (from you too: B, look at
  them, Enter holds yours out), else they keep it wrapped up till they pass
  a bin; the others still flick it away
- Your pocket ashtray: B, Enter at the tin, a bin or the fire empties it
 entropie/Makefile                |  2 +-
 entropie/README.md               | 10 +++--
 entropie/STATE.md                | 10 ++++-
 entropie/commit_message.txt      | 18 ++++-----
 entropie/src/game/game_actions.c | 60 +++++++++++++++++++++++++++---
 entropie/src/people/ai.c         | 80 ++++++++++++++++++++++++++++++++++++++++
 entropie/src/people/ai.h         |  2 +
 entropie/src/people/pockets.c    | 12 +++++-
 entropie/src/people/pockets.h    |  7 +++-
 entropie/src/world/object.h      |  3 ++
 entropie/src/world/scene.c       |  2 +
 entropie/src/world/world.c       | 40 +++++++++++++++-----
 12 files changed, 211 insertions(+), 35 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index cac588a..f2a1637 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.4.0
+VERSION   := 2.5.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 89a05b9..360b6f4 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -542,9 +542,13 @@ say no more often and don't come to you for a bit.
   then the lighter to it - theirs, or "Got a light?" from somebody near
 - A drag now and then (the tip glows), smoke breathed out, the hand up in
   front between drags. Hands needed: it goes into the mouth
-- Done: flicked away or dropped as a butt (into the fire when close; the
-  tidy and you use the old tin on the end of a bench by the fire as ashtray;
-  you see it fill up)
+- Done: into the fire when close, else the tin on the end of a bench by the
+  fire or the bin when near (you see the tin fill up). You and Jana carry a
+  pocket ashtray: the butt goes in there. The tidy walk to the tin, a bin or
+  the fire with it; none near, they ask for a pocket ashtray ("got your pocket
+  ashtray?" - B, look at them, Enter: hold it out) or keep it in a scrap of
+  paper till they pass a bin. The others flick it away or drop it
+- Your pocket ashtray: B, Enter at the tin, a bin or the fire empties it
 - A full ashtray gets emptied by somebody tidy (into a scrap of paper, off
   to the bin)
 - K: light one (yours, or somebody next to you gives you a light), K again
diff --git a/entropie/STATE.md b/entropie/STATE.md
index 7030def..b23e4f6 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -175,6 +175,11 @@ Round 2.4:
 - You (B, pocket rows "Pullover", "Put your pullover down"): put on / take off (round your hips), put it down where the verb line says; click it (within 1.3 m, `takePullover`) to take it back; somebody else's: "That's Jana's pullover". C at your spread pullover: you sit on it. Cold: put on for you once a cold spell (`jacket_told`), after that it's up to you; put down: told where it is.
 - People (`Clothes_update`): sitting down (settled), warm, theirs with them: over the chair's back (0.8 x tidiness) or under them on the ground (tidiness: 0.35 + 0.35 liking for clearing up + 0.2 pride - 0.3 drunk), "Not sitting in the dirt."; getting up, it comes along. Cold with it put down: `ai.c` fetchPullover (idle, wander, at a table; on their own chair or under them: just take it; else goTo it, on with it; gives up after 90 s, 80 m off or warm).
 
+Round 2.5:
+- Cigarette butts (`world.c` smoke, at the end of one): a burning fire within 3 m: in; the tin or a bin within BUTT_REACH 4 m: in (the tidy and you; the others 40 %); a pocket ashtray (`pocket_tray`: you and Jana): `Pockets_keepButt` (`pocket_butts`); the tidy (likes clearing up over 0.2 or pride over 0.4, under 0.6 drunk): `butt_held`; the rest: dropped or flicked (as before).
+- `ai.c` binButt (idle, wander, at a table): with `butt_held`, the tin, a bin or a burning fire within BUTT_WALK 30 m: there (gives up after 40 s: skipped) and in; none: asks somebody within 6 m with a pocket ashtray (NPCs: "Sure, put it in." - into theirs; you: `asking_you` POCKET_ASHTRAY, the hint line, B Enter "hold it out"); nothing after BUTT_WAIT 25 s or held BUTT_KEEP 90 s: wrapped up into the pocket ("I'll keep it till I find a bin."). Pocket butts are emptied passing within BUTT_PASS 2 m of the tin, a bin or the fire; a pocket ashtray with TRAY_FULL 8 is taken there on purpose.
+- Your pockets: row "Pocket ashtray - n butts": Enter looking at somebody holding a butt: it goes into yours ("Thanks - much better than on the ground."); else at the tin, a bin or the fire (within 1 m): emptied.
+
 Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves.
 
 Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built).
@@ -306,6 +311,8 @@ Most important first.
 **Bugs**
 - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm)
 
+- NPC audit: Kurti idles by the fire with the chainsaw in his hand; it knocks against the tripod poles and he jitters (about 1400 of the 3500 joint jumps)
+
 **Body and moving**
 - Obstacle approach, still to do: thin round logs (under 0.2 m) are still stepped over; a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things
 - Climbing rework: hands first (one reaches first, shoulder ahead), a knee up, the other foot next to it; or sit on it and lift the legs; or belly first and roll; by height, surface and person
@@ -319,7 +326,6 @@ Most important first.
 - Smoking, still to do: paying for a pack (no money in the game yet)
 - "No shirt no service" policy (decision open)
 - Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers); the pullover over a railing NPCs never do on their own
-- Butts, next: into a bin or the fire too, a pocket ashtray for some, the tidy ask for an ashtray with no bin near
 - Arrival, next: the first start by car (you, Volkram, Jana, Toni), unloading 2 crates of beer, 2 of water, sleeping bags, mats, tents
 - Tents: Volkram and Jana share one, Toni and you a small one each; pitching together (poles, the cover, pegs); sleeping in them
 - Rain: showers; people go into the house or their tents
@@ -331,4 +337,4 @@ Most important first.
 - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene)
 
 ---
-State of `entropie 2.4.0 (build 6d78a96)`
+State of `entropie 2.5.0 (build 74f2023)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index d96e1f1..f1efa7b 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,11 +1,9 @@
-entropie 2.4.0: pullovers instead of the coat rack
+entropie 2.5.0: cigarette butts go somewhere
 
-- The coat rack is gone (code too); the jacket is now a pullover
-- Your pullover (B): put it on, take it off (tied round your hips), or put
-  it down where you look: over a chair's back, a railing, a wall or a bench,
-  folded on a table, or spread on the ground; click it to take it back
-- Sitting on the ground: spread it out and sit on it (C at it sits on it)
-- Others hang theirs over their chair or sit on it on the ground, take it
-  along when they get up, and go and get it when it gets cold
-- Cold: it goes on for you once, after that it's up to you; put down
-  somewhere, you're told where it is
+- Finished cigarettes go into the fire when close, else the tin or the bin
+  when near; tidy people walk there with the butt
+- Pocket ashtrays (you and Jana): the butt goes in there
+- No bin near: the tidy ask for a pocket ashtray (from you too: B, look at
+  them, Enter holds yours out), else they keep it wrapped up till they pass
+  a bin; the others still flick it away
+- Your pocket ashtray: B, Enter at the tin, a bin or the fire empties it
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 1f21af6..aa4cbfa 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -164,7 +164,7 @@ static void smokeOrStop(Game *game) {
 
 /* ---------- Your pockets (B): what's in them, taken out, given ---------- */
 
-enum { ROW_CIGS, ROW_TOBACCO, ROW_PAPERS, ROW_FILTERS, ROW_LIGHTER, ROW_KEYS, ROW_URINELLAS, ROW_PULLOVER, ROW_PUT_DOWN, POCKET_ROWS };
+enum { ROW_CIGS, ROW_TOBACCO, ROW_PAPERS, ROW_FILTERS, ROW_LIGHTER, ROW_ASHTRAY, ROW_KEYS, ROW_URINELLAS, ROW_PULLOVER, ROW_PUT_DOWN, POCKET_ROWS };
 
 static bool rowHas(const Object *p, int row) {
     switch (row) {
@@ -173,6 +173,7 @@ static bool rowHas(const Object *p, int row) {
     case ROW_PAPERS:    return p->papers > 0;
     case ROW_FILTERS:   return p->filters > 0;
     case ROW_LIGHTER:   return p->lighter;
+    case ROW_ASHTRAY:   return p->pocket_tray || p->pocket_butts > 0;
     case ROW_KEYS:      return p->keys != NULL;
     case ROW_URINELLAS: return p->urinellas > 0;
     case ROW_PULLOVER:  return true;
@@ -189,6 +190,7 @@ static void rowText(const Object *p, int row, char *out, size_t size) {
     case ROW_FILTERS:   snprintf(out, size, "Filter tips - %d", p->filters); break;
     case ROW_LIGHTER:   snprintf(out, size, "%s - %s", p->lighter_of ? "A borrowed lighter" : "Lighter",
                                  p->lighter_uses > 0 ? "works" : "empty"); break;
+    case ROW_ASHTRAY:   snprintf(out, size, p->pocket_tray ? "Pocket ashtray - %d butts" : "Butts wrapped up - %d", p->pocket_butts); break;
     case ROW_KEYS:      snprintf(out, size, "%s", p->keys); break;
     case ROW_URINELLAS: snprintf(out, size, "Urinellas - %d", p->urinellas); break;
     case ROW_PULLOVER:  snprintf(out, size, "Pullover - %s", p->pullover_down ? Clothes_where(p) : p->look.top == TOP_JACKET ? "on"
@@ -213,6 +215,33 @@ static int pocketRow(Game *game) {
     return -1;
 }
 
+/* The tin, a bin or a burning fire within reach: where butts go. */
+static Object *buttPlaceNear(const Game *game) {
+    const Object *p = game->player;
+    for (int i = 0; i < game->world.count; i++) {
+        Object *o = game->world.objects[i];
+        bool takes = (o->type == OBJ_FURNITURE && o->variant == FURN_ASHTRAY) || o->type == OBJ_BIN || (o->type == OBJ_FIREPLACE && o->burning);
+        if (takes && Hands_gapFrom(p->position, p->look.height, o) < 1.0f) return o;
+    }
+    return NULL;
+}
+
+static const char *placeName(const Object *o) {
+    return o->type == OBJ_BIN ? "bin" : o->type == OBJ_FIREPLACE ? "fire" : "tin";
+}
+
+/* Your pocket ashtray (or wrapped-up butts) emptied into the tin, a bin or the fire. */
+static void emptyButts(Game *game) {
+    Object *p = game->player, *place = buttPlaceNear(game);
+    if (!place || p->pocket_butts == 0) { say(game, p->pocket_butts ? "No bin, tin or fire right here to empty it" : "It's empty"); return; }
+    if (place->type == OBJ_FURNITURE) place->stored += p->pocket_butts;
+    Hands_touch(p, Object_colliderCenter(place, 0));
+    p->pocket_butts = 0;
+    snprintf(game->info, sizeof(game->info), "You empty your butts into the %s", placeName(place));
+    game->info_about = NULL;
+    say(game, game->info);
+}
+
 /* What Enter does with that row now: given to who's in front, or used. */
 static void pocketVerb(Game *game, int row, char *out, size_t size) {
     Object *person = game_personInFront(game);
@@ -223,6 +252,13 @@ static void pocketVerb(Game *game, int row, char *out, size_t size) {
     case ROW_PAPERS:    snprintf(out, size, name ? "Enter: give %s a few papers" : "Enter: roll one and light it", name); break;
     case ROW_FILTERS:   snprintf(out, size, name ? "Enter: give %s a few filters" : "Enter: roll one and light it", name); break;
     case ROW_LIGHTER:   snprintf(out, size, name ? "Enter: lend %s your lighter" : "Enter: click - does it work?", name); break;
+    case ROW_ASHTRAY: {
+        const Object *place = buttPlaceNear(game);
+        if (person && person->butt_held && game->player->pocket_tray) snprintf(out, size, "Enter: hold it out for %s's butt", name);
+        else if (place && game->player->pocket_butts > 0) snprintf(out, size, "Enter: empty it into the %s", placeName(place));
+        else snprintf(out, size, "Your butts go in here when there's no bin near");
+        break;
+    }
     case ROW_KEYS:      snprintf(out, size, "Keys stay in your pocket"); break;
     case ROW_URINELLAS: snprintf(out, size, name ? "Enter: give %s one" : "They're for peeing standing (P)", name); break;
     case ROW_PULLOVER:
@@ -271,6 +307,8 @@ static void usePocket(Game *game, int row) {
             else say(game, "You're smoking one already");
         } else if (row == ROW_LIGHTER) {
             say(game, p->lighter_uses > 0 ? "Click - a flame. It works" : "Click, click... it's empty");
+        } else if (row == ROW_ASHTRAY) {
+            emptyButts(game);
         }
         return;
     }
@@ -290,6 +328,12 @@ static void usePocket(Game *game, int row) {
         given = 1;
         snprintf(line, sizeof line, "You lend %s your lighter", person->name);
         break;
+    case ROW_ASHTRAY:
+        if (!p->pocket_tray || !person->butt_held) { emptyButts(game); return; }
+        what = POCKET_ASHTRAY;
+        given = Pockets_give(p, person, what);
+        snprintf(line, sizeof line, "%s puts the butt in your pocket ashtray", person->name);
+        break;
     case ROW_URINELLAS:
         if (p->urinellas > 0) { p->urinellas--; person->urinellas++; given = 1; }
         snprintf(line, sizeof line, "You give %s a Urinella", person->name);
@@ -304,8 +348,8 @@ static void usePocket(Game *game, int row) {
     say(game, game->info);
     if (person->ai.asking_you && (person->ai.asking_you == (int)what || row == ROW_CIGS)) { /* (what they asked for) */
         person->ai.asking_you = 0;
-        person->smoke_ask = 2.0f; /* (and rolls one in a moment) */
-        Ai_say(person, "Thanks, you're a star!");
+        if (what != POCKET_ASHTRAY) person->smoke_ask = 2.0f; /* (and rolls one in a moment) */
+        Ai_say(person, what == POCKET_ASHTRAY ? "Thanks - much better than on the ground." : "Thanks, you're a star!");
     } else if (person->ai.speech_time <= 0.0f) {
         Ai_say(person, "Oh - thanks.");
     }
@@ -747,7 +791,10 @@ static const Object *askingYou(const Game *game) {
     for (int i = 0; i < game->world.count; i++) {
         Object *o = game->world.objects[i];
         if (o->type != OBJ_HUMAN || !o->ai.asking_you) continue;
-        if (Pockets_hasSmoke(o) && Pockets_need(o) == POCKET_NOTHING) { o->ai.asking_you = 0; continue; } /* (got it elsewhere) */
+        if (o->ai.asking_you == POCKET_ASHTRAY ? !o->butt_held : Pockets_hasSmoke(o) && Pockets_need(o) == POCKET_NOTHING) {
+            o->ai.asking_you = 0; /* (got it elsewhere) */
+            continue;
+        }
         if (hypotf(o->position.x - p->position.x, o->position.z - p->position.z) > 8.0f) { o->ai.asking_you = 0; continue; } /* (walked off) */
         return o;
     }
@@ -761,8 +808,9 @@ const char *Game_hint(Game *game) {
     if (game->player->on_fire > 0.0f)
         snprintf(game->hint, sizeof(game->hint), "On fire!  C: drop and roll  -  left click: pour your drink over you  -  water or pee puts it out");
     else if ((asking = askingYou(game)))
-        snprintf(game->hint, sizeof(game->hint), "%s asks you for %s  -  B: your pockets, look at %s, Enter: give it",
-                 asking->name, Pockets_needName((PocketNeed)asking->ai.asking_you), asking->female ? "her" : "him");
+        snprintf(game->hint, sizeof(game->hint), "%s asks you for %s  -  B: your pockets, look at %s, Enter: %s",
+                 asking->name, Pockets_needName((PocketNeed)asking->ai.asking_you), asking->female ? "her" : "him",
+                 asking->ai.asking_you == POCKET_ASHTRAY ? "hold it out" : "give it");
     else if (shown)
         snprintf(game->hint, sizeof(game->hint), "%s follows you: walk to where the firewood should go and left click  -  Backspace: never mind", shown->name);
     else if (watcher)
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 3ee2260..9b9ec72 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -1665,6 +1665,84 @@ static bool fetchPullover(Object *self, Object *const *objects, int count, float
     return true;
 }
 
+/* A finished cigarette in the fingers (the tidy): to the tin, a bin or the
+   fire with it; none near: somebody's pocket ashtray (asked for), else wrapped
+   up and into the pocket. Butts in the pocket (ashtray) go into the tin, a
+   bin or the fire when passing by - or, the ashtray filling up, on purpose. */
+#define BUTT_WALK   30.0f /* m: a place for it this near: walked over */
+#define BUTT_ASK    6.0f  /* m: somebody this near with a pocket ashtray: asked */
+#define BUTT_WAIT   25.0f /* s: asked and nothing came: into the pocket */
+#define BUTT_KEEP   90.0f /* s: still holding it: into the pocket */
+#define BUTT_PASS   2.0f  /* m: passing this near, the pocket's emptied */
+#define TRAY_FULL   8     /* butts: off to empty the pocket ashtray */
+
+static bool buttGoesIn(const Object *o) {
+    return (o->type == OBJ_FURNITURE && o->variant == FURN_ASHTRAY) || o->type == OBJ_BIN || (o->type == OBJ_FIREPLACE && o->burning);
+}
+
+static Object *buttPlaceNear(const Object *self, Object *const *objects, int count, float range) {
+    Object *best = NULL;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (!buttGoesIn(o) || ai_skipped(&self->ai, o)) continue;
+        float d = flatDistance(o->position, self->position);
+        if (d < range) { range = d; best = o; }
+    }
+    return best;
+}
+
+static void buttsInto(Object *self, Object *place, int n) {
+    if (place->type == OBJ_FURNITURE) place->stored += n; /* (the tin fills up; a bin or the fire: gone) */
+    Hands_touch(self, vec3_add(Object_colliderCenter(place, 0), vec3(0.0f, place->type == OBJ_FIREPLACE ? 0.0f : 0.3f, 0.0f)));
+}
+
+static bool binButt(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    if (self->pocket_butts > 0) { /* passing by: emptied */
+        Object *at = buttPlaceNear(self, objects, count, BUTT_PASS);
+        if (at && !self->sitting) { buttsInto(self, at, self->pocket_butts); self->pocket_butts = 0; }
+    }
+    if (!self->butt_held) {
+        if (!self->pocket_tray || self->pocket_butts < TRAY_FULL || ai->mode == AI_TABLE) return false;
+        Object *place = buttPlaceNear(self, objects, count, BUTT_WALK); /* (the ashtray full: emptied) */
+        if (!place) return false;
+        if (goTo(self, place, objects, count, 0.6f, dt)) { buttsInto(self, place, self->pocket_butts); self->pocket_butts = 0; }
+        return true;
+    }
+    ai->butt_time += dt;
+    if (ai->butt_time > BUTT_KEEP || (ai->butt_asked && ai->butt_time > BUTT_WAIT && !ai->asking_you)) {
+        Pockets_keepButt(self);
+        if (random01(ai) < 0.4f) Ai_say(self, "I'll keep it till I find a bin.");
+        return false;
+    }
+    if (ai->butt_asked) { /* (waiting for the player's ashtray) */
+        if (ai->butt_time > BUTT_WAIT) { ai->asking_you = 0; Pockets_keepButt(self); Ai_say(self, "Never mind, I'll keep it."); }
+        return false;
+    }
+    Object *place = buttPlaceNear(self, objects, count, BUTT_WALK);
+    if (place) {
+        if (self->sitting) self->sitting = false;
+        if (ai->butt_time > 40.0f) { skipTarget(ai, place); return false; } /* (can't get there) */
+        if (!goTo(self, place, objects, count, place->type == OBJ_FIREPLACE ? 0.4f : 0.6f, dt)) return true;
+        buttsInto(self, place, 1);
+        self->butt_held = false;
+        if (place->type == OBJ_BIN && random01(ai) < 0.25f) Ai_say(self, "There we go.");
+        return true;
+    }
+    ai->butt_asked = true;
+    Object *giver = Pockets_hasNear(self, POCKET_ASHTRAY, objects, count, BUTT_ASK);
+    if (!giver) return false; /* (keeps it a while: maybe they pass a bin) */
+    if (giver->type == OBJ_PLAYER) {
+        Ai_say(self, "No bin anywhere - got your pocket ashtray?");
+        ai->asking_you = (int)POCKET_ASHTRAY; /* (see the pockets: B, hold it out) */
+        return false;
+    }
+    Ai_say(self, withName("%s, can I use your pocket ashtray?", giver));
+    Pockets_give(giver, self, POCKET_ASHTRAY);
+    if (giver->type == OBJ_HUMAN) Ai_say(giver, "Sure, put it in.");
+    return false;
+}
+
 void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     ai_ensureReady(self);
     Ai *ai = &self->ai;
@@ -1721,6 +1799,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_HELP) && ai_holdLadder(self, objects, count, dt)) return;
     /* Done with a tool: careful people put it back where it belongs, the others leave it lying */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER) && ai_putAway(self, objects, count, dt)) return;
+    /* A finished cigarette in the fingers: to the tin, a bin, the fire */
+    if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && binButt(self, objects, count, dt)) return;
     /* Cold, the pullover put down somewhere: off to get it */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE) && fetchPullover(self, objects, count, dt)) return;
     /* Out of cigarettes: to the machine */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 83ad489..72a3659 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -176,6 +176,8 @@ typedef struct {
     bool           want_smokes; /* out of cigarettes, nobody gives one: off to the machine */
     float          smokes_time; /* ... on the way this long */
     bool           asked_you;   /* asked the player for one (nothing came of it: the machine next time) */
+    float          butt_time;   /* holding a finished cigarette (Object.butt_held) this long ... */
+    bool           butt_asked;  /* ... and asked somebody for their pocket ashtray already */
     int            asking_you;  /* asked the player for this (PocketNeed: a paper, a filter, tobacco), 0: nothing */
     bool           filter_asked; /* asked for a filter this time already (none: rolls one without) */
     float          jacket_time; /* ... on the way this long */
diff --git a/entropie/src/people/pockets.c b/entropie/src/people/pockets.c
index c3776d0..9b77049 100644
--- a/entropie/src/people/pockets.c
+++ b/entropie/src/people/pockets.c
@@ -96,6 +96,7 @@ static int spare(const Object *o, PocketNeed what) {
     case POCKET_PAPERS:  return o->papers;
     case POCKET_FILTERS: return o->filters;
     case POCKET_TOBACCO: return o->tobacco;
+    case POCKET_ASHTRAY: return o->pocket_tray ? 2 : 0;
     case POCKET_NOTHING: break;
     }
     return 0;
@@ -113,7 +114,7 @@ Object *Pockets_hasNear(Object *who, PocketNeed what, Object *const *objects, in
 }
 
 int Pockets_give(Object *from, Object *to, PocketNeed what) {
-    static const int SOME[] = { [POCKET_NOTHING] = 0, [POCKET_PAPERS] = 3, [POCKET_FILTERS] = 5, [POCKET_TOBACCO] = 2 };
+    static const int SOME[] = { [POCKET_NOTHING] = 0, [POCKET_PAPERS] = 3, [POCKET_FILTERS] = 5, [POCKET_TOBACCO] = 2, [POCKET_ASHTRAY] = 1 };
     int n = spare(from, what);
     if (n > SOME[what]) n = SOME[what];
     if (n <= 0) return 0;
@@ -121,6 +122,7 @@ int Pockets_give(Object *from, Object *to, PocketNeed what) {
     case POCKET_PAPERS:  from->papers -= n;  to->papers += n;  break;
     case POCKET_FILTERS: from->filters -= n; to->filters += n; break;
     case POCKET_TOBACCO: from->tobacco -= n; to->tobacco += n; break;
+    case POCKET_ASHTRAY: if (!to->butt_held) return 0; to->butt_held = false; from->pocket_butts++; break;
     case POCKET_NOTHING: break;
     }
     return n;
@@ -131,6 +133,7 @@ const char *Pockets_needName(PocketNeed what) {
     case POCKET_PAPERS:  return "a paper";
     case POCKET_FILTERS: return "a filter";
     case POCKET_TOBACCO: return "a bit of tobacco";
+    case POCKET_ASHTRAY: return "your pocket ashtray";
     case POCKET_NOTHING: break;
     }
     return "something";
@@ -147,5 +150,10 @@ int Pockets_slots(const Object *who) {
 
 int Pockets_used(const Object *who) {
     return (who->cigarettes > 0) + (who->tobacco > 0) + (who->papers > 0) + (who->filters > 0) +
-           (who->lighter ? 1 : 0) + (who->keys ? 1 : 0) + (who->urinellas > 0);
+           (who->lighter ? 1 : 0) + (who->keys ? 1 : 0) + (who->urinellas > 0) + (who->pocket_tray || who->pocket_butts > 0);
+}
+
+void Pockets_keepButt(Object *who) {
+    who->pocket_butts++;
+    who->butt_held = false;
 }
diff --git a/entropie/src/people/pockets.h b/entropie/src/people/pockets.h
index 7768a4d..54aab56 100644
--- a/entropie/src/people/pockets.h
+++ b/entropie/src/people/pockets.h
@@ -32,15 +32,18 @@ bool    Pockets_giveSmoke(Object *from, Object *to);
 
 /* What who lacks to roll one (they have tobacco or papers, not both - or no
    filter and they use them): POCKET_NOTHING when they can, or have ready ones. */
-typedef enum { POCKET_NOTHING, POCKET_PAPERS, POCKET_FILTERS, POCKET_TOBACCO } PocketNeed;
+typedef enum { POCKET_NOTHING, POCKET_PAPERS, POCKET_FILTERS, POCKET_TOBACCO, POCKET_ASHTRAY } PocketNeed;
 PocketNeed  Pockets_need(const Object *who);
 /* Somebody within range who has that to spare (not who). */
 Object     *Pockets_hasNear(Object *who, PocketNeed what, Object *const *objects, int count, float range);
-/* Gives some of it (3 papers, 5 filters, a pinch for 2): how many. */
+/* Gives some of it (3 papers, 5 filters, a pinch for 2; the pocket ashtray: to's butt goes into from's): how many. */
 int         Pockets_give(Object *from, Object *to, PocketNeed what);
 /* "a paper", "a filter", "a bit of tobacco" */
 const char *Pockets_needName(PocketNeed what);
 
+/* A finished cigarette: into who's pocket ashtray (or wrapped up in the pocket: tidy ones without one). */
+void    Pockets_keepButt(Object *who);
+
 /* Pockets: how many (by what they wear) and how many have something in them. */
 int     Pockets_slots(const Object *who);
 int     Pockets_used(const Object *who);
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index 2329f8e..718d281 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -203,6 +203,9 @@ typedef struct Object {
     int        papers;        /* ... rolling papers (a packet: PAPERS_PACK) ... */
     int        filters;       /* ... filter tips (a bag: FILTERS_BAG) ... */
     bool       filter_habit;  /* ... rolls with a filter (without one they ask for one first) */
+    bool       pocket_tray;   /* ... a pocket ashtray (a little tin with a lid) ... */
+    int        pocket_butts;  /* ... butts in it (or, without one, wrapped up in the pocket): emptied at a bin, the tin or the fire */
+    bool       butt_held;     /* characters: a finished cigarette in the fingers, looking for where it goes (ai.c binButt) */
     const char *keys;         /* ... keys in the pocket ("Car keys"), or NULL */
     bool       work_trousers; /* the player: work trousers, more pockets (see Pockets_slots) */
     float      smoker;        /* 0 = doesn't smoke .. 1 = chain smoker: how fast the craving grows */
diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c
index d0bb9ec..7c41ad9 100644
--- a/entropie/src/world/scene.c
+++ b/entropie/src/world/scene.c
@@ -97,6 +97,7 @@ static const int   TOBACCO[]    = { POUCH_ROLLUPS, 40, 10, 25, 60, 0, 0 };
 static const int   PAPERS[]     = { PAPERS_PACK, 20, 5, 0, 30, 0, 0 };   /* (Toni forgot his) */
 static const int   FILTERS[]    = { FILTERS_BAG, 60, 0, 15, 0, 0, 0 };   /* (Jana is out) */
 static const bool  FILTER_HABIT[] = { true, true, true, true, false, false, false }; /* (Kurti: none, ever) */
+static const bool  POCKET_TRAY[]  = { true, false, true, false, false, false, false }; /* a pocket ashtray: you and Jana */
 static const char *const KEYS_IN_POCKET[] = { "Car keys", NULL, NULL, NULL, "House keys", NULL, NULL };
 static const int  GARMENT[]  = { GARMENT_TROUSERS, GARMENT_TROUSERS, GARMENT_SKIRT, GARMENT_SHORTS, GARMENT_TROUSERS, GARMENT_TROUSERS, GARMENT_SHORTS };
 /* Years: past 45 slower on their feet (Kurti is 74). */
@@ -252,6 +253,7 @@ Object *Scene_load(World *world) {
             obj->papers     = PAPERS[characters % TEMPERAMENT_COUNT];
             obj->filters    = FILTERS[characters % TEMPERAMENT_COUNT];
             obj->filter_habit = FILTER_HABIT[characters % TEMPERAMENT_COUNT];
+            obj->pocket_tray  = POCKET_TRAY[characters % TEMPERAMENT_COUNT];
             obj->keys       = KEYS_IN_POCKET[characters % TEMPERAMENT_COUNT];
             obj->work_trousers = characters == 0; /* (you: work trousers, plenty of pockets) */
             obj->craving    = 0.3f + 0.15f * (float)(characters % 3);
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 09d08d1..f03aba6 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -1,6 +1,7 @@
 #include "world/world.h"
 #include "world/camp.h"
 #include "world/clothes.h"
+#include "people/pockets.h"
 #include "people/condition.h"
 #include "people/dog.h"
 #include "world/forestry.h"
@@ -659,8 +660,24 @@ static void overFeet(Object *o) {
 
 #define FAR_DETAIL 30.0f /* m from the player: beyond, feet and hands don't look out for things */
 
-/* Smoking: puffs breathed out and off the tip, the butt when it's done (into a
-   burning fire close by, else on the ground). */
+/* Smoking: puffs breathed out and off the tip, the butt when it's done: into a
+   burning fire close by, the tin or a bin near (the tidy, and you), into a
+   pocket ashtray; the tidy keep it in the fingers and go and find a place for
+   it (ai.c binButt); the others drop it or flick it away. */
+#define BUTT_REACH 4.0f /* m: the tin or a bin this near */
+
+/* Where a butt goes: the tin or a bin within reach. */
+static Object *buttPlace(const World *world, const Object *o, float reach) {
+    Object *best = NULL;
+    for (int i = 0; i < world->count; i++) {
+        Object *t = world->objects[i];
+        if (!(t->type == OBJ_FURNITURE && t->variant == FURN_ASHTRAY) && t->type != OBJ_BIN) continue;
+        float d = vec3_length(vec3_sub(t->position, o->position));
+        if (d < reach) { reach = d; best = t; }
+    }
+    return best;
+}
+
 #define CRAVE_TIME 300.0f /* s from nothing to a strong craving, for a chain smoker */
 
 static void smoke(World *world, Object *o, Vec3 facing, float dt) {
@@ -670,14 +687,19 @@ static void smoke(World *world, Object *o, Vec3 facing, float dt) {
     if (Body_takeButt(&o->body)) {
         Vec3 at = Figure_toWorld(o->position, facing, sk->cig_glow >= 0.0f ? sk->cig_tip : sk->joints[JOINT_HAND_R]);
         Object *fire = Camp_fireNear(world->objects, world->count, o->position, 3.0f, true);
-        Object *tray = NULL; /* (an ashtray near: the tidy and you put it in there) */
+        Object *tray = buttPlace(world, o, BUTT_REACH);
         bool tidy = o->type == OBJ_PLAYER || (o->type == OBJ_HUMAN && (o->ai.character.likes[ORDER_TRASH] > 0.2f || o->temperament.pride > 0.4f) && o->drunk < 0.6f);
-        for (int i = 0; i < world->count && tidy; i++) {
-            Object *t = world->objects[i];
-            if (t->type == OBJ_FURNITURE && t->variant == FURN_ASHTRAY && vec3_length(vec3_sub(t->position, o->position)) < 4.0f) tray = t;
-        }
-        if (tray) tray->stored++;
-        else if (!fire) {
+        if (fire) {
+            /* (flicked into the flames) */
+        } else if (tray && (tidy || worldDice(world) < 0.4f)) {
+            if (tray->type == OBJ_FURNITURE) tray->stored++; /* (a bin: in it, gone) */
+        } else if (o->pocket_tray) {
+            Pockets_keepButt(o);
+        } else if (tidy && o->type == OBJ_HUMAN) {
+            o->butt_held = true; /* (off to find a place for it, or ask) */
+            o->ai.butt_time = 0.0f;
+            o->ai.butt_asked = false;
+        } else {
             Object *butt = Object_create(OBJ_TRASH, PALETTE_WARM, at, vec3(-1.0f, 0.0f, 0.0f), 0.005f);
             if (butt) {
                 snprintf(butt->name, sizeof(butt->name), "Cigarette butt");