Skip to content

Seating a party

A party is the people in one booking. When a resource runs in a venue with numbered places, something has to decide which seats they get, and the decision is not "any available seat": a family that arrives in one order and sits in four different places has been served badly, however many seats were available.

That decision is a plugin, so a house that disagrees can replace it without touching the engine. This page is the rules the plugin shipped with yoyaku follows, and the reasoning behind each one, because most of them look arbitrary until you see the case that motivated them.

What makes two seats neighbors

Every rule below rests on one requirement about the hall: two places touch when they are in the same row of the same area and their positions are consecutive. A run is consecutive positions, a fragment breaks where they skip, and a party is seated together by taking them side by side. A position is therefore not a label and not a sort key. It is the claim that two seats are next to each other, and nothing downstream can check it.

Two things follow, and both have bitten this module.

The order has to be the row's own order. A hall drawn on a plan is easy to number by sorting on x, which is right for a straight row and wrong for every row that turns: a balcony sweeping round the hall runs out of width and starts to climb, so an x-sort interleaves its two halves and seats at opposite ends of the sweep end up holding consecutive positions. Eleven rows of the bundled Auditorium de Bordeaux package were numbered that way, and its worst pair sat 1986 map units apart calling itself adjacent. A party of twelve booked across three categories was seated in two clumps a third of the hall apart and told it was sitting together.

So the numbering is derived from the place coordinates rather than trusted from a field. Each place's x and y are the center of its seat, so the distance between two of them is center-to-center spacing: a seat pitch. (Not the venue's map background, which is an SVG nothing here parses.)

RowPath walks a row from one of its two ends — the two seats furthest apart — to the nearest seat not yet walked, which recovers the order of any row laid out as evenly spaced seats however it bends. Which end comes first is not a geometric question, so the direction the venue already numbers in is kept: a hall's two sides mirror each other, and one side counting the other way is correct rather than a fault.

This is also why the printed place number cannot be the ordering. A parterre numbered outwards from the middle puts seats 1 and 2 beside each other at the center, so ordering by number would seat a party across the aisle; 35 of the Auditorium's rows are numbered that way. Some places are not numbered with numbers at all (35b).

The numbers have to run 1, 2, 3 within one area. A section with subsections is several areas, and a run is only read inside one of them, so a printed row numbered straight through when part of it belongs to a subsection leaves each half full of holes and no run can be found in either. The Auditorium's balcony had positions 1 to 9, then 12, 15, 19, 21 in one area, so a row of thirty-four free seats could not offer a run longer than nine.

A subsection boundary therefore stops a party being seated across it, and that is right: in this hall the balconies are partitioned boxes, and the gap in the plan is a wall. A venue that cut a row mid-pitch would be describing a hall it does not have.

A row is not always evenly spaced. An aisle, a step, or a seat that simply does not exist leaves two places next in the numbering with something between them, and consecutive positions then claim an adjacency the hall does not have. So a place also carries gap_before: whether a physical break separates it from the previous position in its row.

Like the numbering, it is read off the coordinates rather than maintained by hand, and by the same walk: a step is a break when it is at least 1.8 times the row's own median step. Measured against that row rather than the hall, because a balcony's seats sit closer together than a parterre's, and a row curving away from the center of the plan has a shorter chord between neighbors than a straight one.

The threshold is a cliff rather than a judgement, at least on the venue this was built against. Of 1300 consecutive pairs in the Auditorium, 1255 sit under 1.2 pitches and 38 over 1.8, with two anywhere in between. It is also deliberately conservative: calling a break where there is none refuses seatings that are genuinely together, which is worse than missing one, so an ambiguous step stays a neighbor.

Nothing reads the flag yet. Writing it down and drawing it is what lets a venue be checked before it decides a booking.

Checking and repairing a venue's numbering

Neither failure is visible from a list of places, so there are two ways to see it:

  • the venue's Numbering tab draws the hall with each row threaded in position order. A row numbered along itself is a line the length of the row; a row numbered across it doubles back once per bend. A marker shows the seat holding position 1, so the direction is visible too.
  • drush yoyaku:venue-positions <venue> names the rows that disagree with the coordinates, with the widest gap each one calls adjacent. It exits non-zero when it finds any, so a deployment can gate on it. Add --fix to repair them.

Both compare the hall against its coordinates, and the comparison is not shown until it is asked for: the Compare with the coordinates button at the foot of the Numbering tab. Every row it lists differs in at least one way, and the columns say which:

Column What it says
Numbering Whether the row runs 1..N in the order the coordinates give. in order means the numbering is right and the row is listed for its breaks alone.
Order the coordinates give The place numbers in that order, filled in only when the row is not already in it. This is what --fix would write.
Breaks set The seats the hall currently has a gap before, whether imported or ticked by hand.
Breaks the coordinates suggest The seats the spacing puts a gap before, at 1.8x the row's own seat pitch.
Widest gap treated as adjacent The largest distance the row still calls two seats side by side, in the units of the plan. A break is what stops it.

The two break columns differing is ordinary. The coordinates only see spacing, so a plan drawn without an aisle's width in it cannot suggest that aisle, and somebody who ticked the box knew the hall better than the map does. That is why --fix writes positions and leaves breaks alone: only --fix --breaks overwrites them, and it discards every hand correction when it does.

The numbering column is the one that is a fault. A row not in the order its coordinates give is claiming adjacency it does not have, and no amount of local knowledge makes that right.

Importing a package changes nothing. It takes the manifest as written, positions and breaks alike, and a package carries both: they decide which seats are adjacent, so a file that stated one and left the other to be worked out would describe a hall whose aisles nobody wrote down.

Reading a hall out of its coordinates is a deliberate operation and only that. An import that quietly corrected the file would leave the venue describing something nobody wrote, and the file would stop being what the hall is. So a package that disagrees with its own coordinates imports as it stands, the Numbering tab says so, and the drush command exits non-zero until somebody decides.

The repair saves only the places that actually move, so a hall already numbered along its rows is left alone and running it twice changes nothing.

Which strategy seats a slot

One strategy applies per slot, and the hall answers for it: its own seating_strategy field. A hall holding inherit there, or holding nothing, is answered by the level that rules the field through its field rules — its resource type, then its tenant, then the site — so a house seating every hall one way names it once, at whichever level it means it.

Nothing named anywhere seats a party together, which is what a house wants until it says otherwise. That default lives in SeatingStrategyManager, so a new site is already seating parties together before anybody visits a settings form.

Deliberately not per slot. A run of a show seats its parties the way the hall does, and letting one evening differ would add a setting nobody sets and everybody has to read past. The seam is there if a production ever needs it.

The one thing a strategy may never do

A place the visitor chose on the map is never moved. They asked for that seat. A strategy may give up a place the visitor was merely given, but it has to say which bookings it gave up, and the surface then has to tell the visitor their places moved. A better seat nobody asked for is still a surprise.

Everything below is bounded by that rule. Where a step could only work by moving a chosen place, the step is skipped rather than bent.

Seating a party: the ladder

Seven attempts, in this order, and the order is the whole design. The first five keep the party as close together as the house allows. The last two are what happens once nowhere holds all of them: in how few runs can they sit, and then where is there room at all.

Together::propose() walks them. Each box below is one attempt, and the first one with an answer is the answer: nothing further is tried.

flowchart TD
    START[propose: the ask, and where<br/>the party already sits]
    START --> EXT{extend:<br/>free seats touching the party?}
    EXT -->|yes| DONE_EXT[take them, nobody moves]
    EXT -->|no| HASP{a party, and no seat it chose?}
    HASP -->|no party, or a chosen seat| KEPT
    HASP -->|yes| REPL{re-place<br/>one unbroken run for ALL of them?}
    REPL -->|yes| DONE_REPL[give every place up, take that run]
    REPL -->|no| KEPT[seat what fits beside them,<br/>the rest as close as it gets]
    KEPT --> CHOSE{a party, and no seat it chose?}
    CHOSE -->|no| DONE_KEPT[keep that]
    CHOSE -->|yes| WHOLE[reseatWholeParty:<br/>seat all of them from scratch]
    WHOLE --> G1{seated every one of them?}
    G1 -->|no| DONE_KEPT
    G1 -->|yes| G2{fewer areas than staying?}
    G2 -->|no| DONE_KEPT
    G2 -->|yes| G3{no more runs, and no worse closeness?}
    G3 -->|no| DONE_KEPT
    G3 -->|yes| DONE_MOVE[move the whole party]

Two things in that picture are easy to get wrong.

Re-place is what usually moves a party, not the whole-party move. Both give every place up, but re-place asks for one unbroken run and returns the moment it finds one. A party of seven carried from a full room into a room with seven free seats side by side is re-place's work. reseatWholeParty() is only reached when no run in the house is long enough, which is every party longer than the longest row.

The three gates guard against the move being worse, not against it being unnecessary. reseatWholeParty() hands the seating a pool that includes the party's own places, having forgotten where they sit, so it chooses the arrangement freshly. The gates then weigh that against what staying would have given: everybody seated, fewer areas, and no more runs. Fail any one and the party stays.

A move is accepted at the last gate, so it has to be reached for the party to move at all. In practice it seldom is. The seating takes the fewest runs it can find, from a pool that always includes the party's own places, so its candidate usually matches what staying would give and the gate above stops things first.

Reaching it takes a hall with nothing else to try. The party is longer than every row, so re-place cannot seat them in one run. They fill their own area, so nobody can sit beside them. And another area can hold them all in fewer pieces than staying would.

Anything less is answered further up the ladder. That is worth knowing when reading a test: a party can end up in one area without this rung running at all, so counting the areas it occupies proves nothing. Name the area on both sides instead.

Whether the last gate can ever refuse a move is open. The rule on lone places can reject the fewest-runs arrangement, which would leave the candidate more broken up than staying. That is the one shape the gate exists to turn down.

1. Extend

Available places immediately beside what the visitor already holds, all of the ask or none of it. Beside means touching: a seat two along, with a stranger in between, is not sitting together. Either end of the run will do, and every row the party occupies, so a party already spread over two rows grows in both.

An available seat inside the party is taken before either end of it. That is the one available seat which makes the party more together rather than merely bigger.

A gap is a seat with a held place on both sides of it, and it is preferred outright rather than by being met first. Reading the row from one end and taking whatever touched the party looked like the same rule and was not: a party on 2, 3, 5 and 6 with a hole at 4 and a free seat at 1 grew into 1, leaving them in two runs around the hole (#3615216). It is asked again after each seat, because taking one can turn the seat beyond it into a gap.

This is what makes three clicks on a stepper behave like one ask for three, and it moves nothing, so the visitor is told nothing.

2. Re-place

The best run for the whole party, giving up the places the visitor was given and taking better ones. Off the table entirely once one of their places was their own choice.

A re-place carries each seat's tariff across with it. This matters more than it sounds: get it wrong and a visitor adding one reduced ticket has their full-price seats quietly turned into reduced ones, and the house loses the difference on every one of them.

3. Grow, and seat the remainder

What fits beside the party, plus the rest wherever the rungs below can put it. The answer when the party can have neither all of it beside them nor a run of their own to move into.

Without this step an ask the seats beside the party cannot swallow whole goes to the next rung entire, and that rung answers the whole ask in one place: asked for five with two seats left at the end of the party's row, it leaves both empty and seats all five in the next row. Adding the same five one click at a time filled them, because each single one fitted, so the fault looked like one about how fast the visitor clicked.

The remainder goes down the ladder from where the party sits including the seats just taken, so it lands beside them rather than beside where the party used to end. Nothing is released, so nothing the visitor already holds moves, but the party does end up in more than one run and is told so.

4. Near the party

The additions as close as possible to what the party already holds, read from where the party is sitting rather than from the house: its own row first, then its own subsection, then its own section. Within a rung the candidates are ordered by distance, so a seat one row back is taken before one twenty rows back.

Row distance is a row's place among its area's rows, counted over the party's own rows as well as the free ones. Counting only the rows that still have a seat left fails on the case the rung exists for: a party is seated near its row because that row is full, a full row has no seat to be counted by, so every row measured as infinitely far and the order fell through to how far along a row a seat sits. A seat ten rows back at the party's own position then beat the row immediately behind them (#3615216).

A section with no subsections has no subsection rung. Its area is the section, so that attempt is skipped and the section answer stands, which is also the honest word to use in the message.

Tried before any fresh run elsewhere, because two seats side by side in another section leave whoever already holds one watching from across the house.

5. Adjacent

Side by side somewhere new. This is the first click's whole story, and also the answer when the party holds nothing to stay near.

6. Fewest runs

In as few runs as the party can be broken into, crossing areas if that is what it takes.

Every rung above this one is about how close the places are. None of them is about how many runs the party ends up in, and the two are different questions with different answers. Spreading a party answers the first one and hands out places in order, so six people with no run of six anywhere sit in one row and every one of them sits between strangers, while two threes in two areas would have been two runs of three. That is better for those six people, and until this rung the ladder had no way to say so (#3617891).

So the runs are counted first and the closeness second. Two threes in two areas beat six singles sharing a row; two threes in one row beat two threes in two areas. The parts of the house are weighed nearest first, the same ones spreading uses, and a part of it only wins by breaking the party into strictly fewer runs than one already found.

Which stretches to cut the runs from needs no search: a run can be any part of one unbroken stretch, so the longest stretches are taken until they cover the ask, and no smaller number of them could.

How long each run is does need deciding, and the answer is evenly. A party of six broken in two would rather be two threes than a five and one person on their own, which is the complaint the rung exists to answer. The rule on leaving no place on its own narrows what each run may take, which is why the lengths are worked out rather than dealt out: a stretch of four, in a house that leaves no place on its own, can give one, two or all four of its seats and never three. Where a number of runs has no legal split at all, one more run is tried.

A party of one skips the rung: one place is one run wherever it goes.

7. Spread

Wherever there is room, together or not. One row with gaps first, since that is what a party notices next after sitting side by side.

Still the floor under the rung above, and not only for the case where nothing fits. Spreading hands a whole part of the house to the share-out at once, so a party across tariffs that cannot be priced run by run may still be priced here.

What the visitor is told

How close together the party ended up is not a yes or no. Four seats in one row with a stranger in the middle is a different disappointment from two in the stalls and two in the balcony, so the answer is a rung:

Rung Meaning Said out loud
Adjacent Side by side, no gaps Nothing
Same row One row, with a gap in it Yes
Same area One area, across rows Yes
Same section One section, across its subsections Yes
Scattered Anywhere else Yes

How much of that ladder a booker hears is the house's own answer, on seating_messages. It names the rung the house starts speaking at: everything (the default), from "not side by side" down, from "not all in the same row" down, from "not all in the same part of the section" down, only a party spread around the venue, or nothing. A hall whose rows are its geography wants all of it; a standing room where a row names nothing is describing a distinction its bookers do not have. The site answers on the booking settings form, and a tenant, a resource type or one resource may answer for itself the way every ruled field is answered: a resource is asked the question on its own form and starts out inheriting it.

A house that does not want the question asked per production does not have to carry it. The rule that sets the answer also says how the field is shown, so a tenant naming a value and hiding it takes the control off the resource form and applies its own answer everywhere under it; read-only leaves it visible and says which level decided. That is the same mechanism every ruled field has, and it is why this one needs no switch of its own to be a house-wide voice.

The setting governs a booking that worked. A booker refused a place is told why whatever it says, because a gesture that fails silently is worse than one that explains itself.

The sentence for a party that moved into a row it was not in is not a rung: a party that moved as a unit is as together as it was, so it is said only where the house says everything.

The rung is judged over the whole party, not over the seats just taken. A run of twelve taken while one held seat sits in another section is not a party sitting together, and reporting the run's own rung would tell the visitor nothing was wrong while one of them watches from across the house.

The rung is the whole message, and it says how far apart the places are rather than how many runs the party sits in. Deliberately: the sentence is what a booker reads beside one tariff, so a count in it is a count they cannot reconcile with anything on their screen, and "the same row, but not all next to each other" is true of two threes in that row as much as of six seats on their own. The plan shows which it was, and a venue that wants the point made out loud attaches the acknowledgement below.

A section with no subsections never reports "same section": it has no parts, so "not in the same part of it" would be a claim about the venue that is not true. Its answer is the area rung, whose sentence is "the same section, but not all in the same row", which is exactly what happened.

A message is easy to miss, though, and it is shown on the page that placed the party rather than where they commit. A venue that wants more than a message attaches Acknowledge places that are apart, which puts the same requirement on the cart page as something the booker has to tick before checking out. The two cannot disagree about what side by side means: both read PlaceGeometry::getPartyRuns().

None of these sentences carries a number. Three different counts are in play when one is shown: the tariff's own quantity on the control the visitor clicked, the whole party across every tariff, and the places just added. Only the first is on screen, and it is the one the sentence is not about, so printing any figure invites the visitor to reconcile it with the stepper and fail. The count is still read, because both languages need to know whether to say "your place" or "your places".

Mixed tariffs

A seat is only bookable at a tariff that prices its grade. A party booking two full prices and two youth tariffs is therefore not four interchangeable requests: the seats have to be matched to the tariffs.

Handing them out first come refuses arrangements that exist. Give the two full prices the first two seats of a run and the youth tariffs may have nowhere left to sit, though a different split of the same four seats would have worked. So when the requested tariffs price different grades, the seats are matched by augmenting paths, which finds an arrangement whenever one exists. That matching is what seats two adults and two children in one row instead of two pairs across the hall.

When every requested tariff prices the same grades, which is the usual case, any split works and the matching is skipped entirely. The expensive path is only taken when it can change the answer.

Reducing a party

The half that is easy to forget. A party seated perfectly is broken up again the moment the visitor steps a quantity back down and the wrong seats are the ones handed back. Giving them back in the order they happen to load takes them out of the front of the party's own run, which leaves a hole in the middle of a row and, if the party had a stray pair elsewhere, keeps the stray and eats the run.

So a reduction is a seating decision too, and the same strategy makes it. It works on fragments: the unbroken runs the party is currently sitting in, computed across the whole booking rather than one tariff of it, since a run of seats can be held at three different prices and giving up the tariff in its middle leaves a hole whatever the arithmetic says. A place in a pool has no neighbors, so it counts as a fragment of one. A place in an assigned area has neighbors like any other: the house chose it, and the row it sits in is the row a booker sees.

Two passes:

  1. Clear whole fragments, smallest first, while the reduction can still afford them. Clearing is the only move that reduces how many runs the party is in, every cleared fragment counts the same, and the budget is the only constraint, so taking the cheapest ones first clears as many as any other choice could.
  2. Peel the ends of the fragment the party got last with whatever is left, and then the one before it. Peeling an end shortens a fragment without splitting it, so it cannot undo the first pass, and the last fragment is the spill: the seats taken once the row the party started in had run out. Shrinking the spill first is what keeps the run behind it packed against its row.

Peeling the largest fragment instead would keep the smallest survivor bigger, so nobody sits on their own, and it reads better stated that way. It is wrong on the case that matters. A party filling a row of sixteen and spilling five into the row behind, reduced by four, comes back as twelve seats, four empty ones at the end of their own row, and five people still sitting behind the gap. Shrinking the spill gives the full row of sixteen and one seat immediately behind it: the same number of people, and no hole. That was a real defect, found on a seat map after the rule had been written the other way round.

Between two fragments of the same size it takes the one sitting furthest from the others: the pair stranded in another section is the pair the party misses least. Distance here is coarser than the row-by-row ranking used when placing, on purpose. That ranking counts a row's place among its area's rows, and a reduction has no reason to read the house at all, so this is measured from the seats the party is already holding: along a row, then a row away, then elsewhere in the area, then elsewhere in the section, then another section.

Everything then ties on the seats added most recently, so a party sitting in one unbroken run, where none of the fragment rules have anything to say, simply hands back the last clicks. A place the visitor chose goes last of all, after every seat that can be handed back without touching their choice.

"Last of all" is over the whole party, not over a fragment, and the difference is not academic. Ranked inside its own fragment, a chosen place came out ahead of every seat of every later fragment, so a booker holding their own pick with two seats beside it and four the house gave them further along the row lost the seat they had picked when they stepped down by three (#3615216). The chosen places are therefore held out of the fragment passes entirely and appended once every fragment has had its say. Clearing a fragment never touches one, because a fragment holding a chosen place is not clearable in the first place.

Worked example, taken from a real order. The visitor holds sixteen seats filling row M and six more in row N, and steps the quantity down by eight:

  • pass one clears row N, six of the eight;
  • pass two peels two ends off row M, taking the newer end first.

Fourteen seats survive in one unbroken run. Handing back the oldest eight bookings instead would have left eight seats in row M and six in row N: a party of fourteen in two runs, having asked only to book less.

Closing a hole

Ordering which seats come back keeps a whole party whole. It cannot make a split party whole again, and nothing above can, because handing seats back only ever removes: a party of ten in one run, one seat released out of the middle of it, then the quantity stepped down by one, gives back an end and stays split around the hole. The empty seat in the middle of them then stays open for a stranger to book, on a house that advertises seating a party together. That was reported from a seat map (#3615212).

Closing that hole means taking a place as well as giving places up. That is a re-place, so it is a third question, and consolidate() asks it. It runs after every change to what a booker holds on a slot. In the common case it answers nothing, which Together::canBeImproved() settles from the party's own places before anything is read.

A hold asks for one itself, before it releases its locks. The consolidation then lands in the transaction that made it necessary, and no other request ever sees the split arrangement. A hold can only consolidate what its own locks cover, which is the area the party sits in. Anything further is left to consolidate(), which takes the locks it needs.

Stepping down is not a hold at all, so there the separate call is the only one. And stepping down is exactly what leaves a hole in the middle of a party.

The party keeps its size, so the run it needs is exactly as long as the party is, and every run that long in the house is a candidate. Which one to take is decided by a single measure: how much of the party is already sitting in it. The more of them a run covers, the fewer people move, and that is the ranking a party would choose for itself.

That one measure settles two things that would otherwise be rules of their own. A row the party already sits in beats an empty row elsewhere, so a hole never becomes a reason to move everybody to the balcony; and among the windows of one long empty stretch it picks the one against the party rather than the first one in the row. A run that cannot be shared out among the party's tariffs is no use however much of the party it covers, so the ranked runs are tried in turn.

The overlap can never be the whole party, since a split party has no run holding all of it, so a consolidation always moves at least one person. It says nothing about it, unless the party ends up in a row it was not in. A booker asking for a quantity names no place: every place they hold is the seating's own choice, so exchanging some of those choices for others changes nothing the booker expressed, and a sentence about it would describe a gesture they never made. Which ROW they sit in is another matter, since a house has a front and a back, so a party that arrives in a row it did not hold is told "Your places are in another row now." A party pulled out of two rows into one of them has left nothing and hears nothing, because every row it ends in is a row it was in.

That sentence is said only where the party ends up together, in one row. It is written in the singular and describes a party that moved as a unit, so beside a party fanned out over several rows it would assert what the closeness sentence next to it denies. Where the closeness sentence speaks about rows itself, as "Your places are in the same section, but not all in the same row." does, it is the one that survives: it says where the party is sitting rather than how it got there, and one sentence about the rows is enough.

A refusal is silent too, and for a stronger reason. The exchange is all or nothing inside one transaction, so a rearrangement that fails leaves the booker holding exactly the places and the very bookings they held a moment earlier: they asked for a quantity, they have it, and nothing of theirs was taken. That their party sits apart is true, and it was already said in their own words by the closeness sentence on the claim that placed them, so saying it again here would repeat it on every press of a stepper that failed to improve on it. The silence is honest only while the exchange stays all or nothing, which is why SeatTogetherTest asserts the bookings are identical after a refusal rather than only that nothing was said.

The question is answered from the candidates the strategy was handed and handed back, which carry their row, so it costs no query and no entity load: this runs inside the hold's locks, where a read would be paid by everybody queued behind it. What is also reported is how close together the party ended up, a requirement about the outcome rather than about the route to it, and the one place a booker can name that does move: a party moved into an area they asked for is told so in those words. Three answers are "leave them alone", and each is load-bearing:

  • the party is already in one run, which is what this exists to reach;
  • one of its places was chosen by the booker, since taking a run means giving up every place the party holds, including theirs;
  • no run long enough exists, which is usually why the party is split in the first place. Shuffling it would move people for nothing.

A unit in a pool is standing in an area rather than sitting anywhere in it, so it is neither part of a hole nor able to close one: a party half in a pool has its seated half consolidated and the rest left where it is.

What comes back is an exchange, held in one atomic step under the slot lock: the seats given up and the seats taken move together, so the units never leave the order and nobody can slip into the place being vacated. It is also what makes it safe to try at all. If the hole is taken between the read and the lock, the hold fails and the booker simply keeps what they had, which is the arrangement they would have kept anyway, so a refusal is reported rather than thrown.

One consequence for the engine, since an exchange that keeps a place is new: the fast-reject pass above the locks no longer runs the per-booking constraints for an exchange, only the authoritative pass under them, after the release. Every seat the party keeps reads as taken until it is given up, so checking availability before the release refuses a party for holding its own seats.

Leaving no place on its own

A venue can refuse to leave a single place available by itself: nobody chooses the seat between two bookings, and nobody chooses the last seat left at the end of a row either, so the row loses it for the slot. The rule is a constraint policy, "Places a booking may orphan", attached to a resource or its type and configured with one number, the fewest places that may be left available side by side. See Constraint policies.

Both shapes are one measurement. A place alone between two bookings is a stretch of one bounded by bookings; a place alone at the end of a row is a stretch of one bounded by a booking and the row's edge. What bounds a stretch does not change its length, so this is one number and not two rules, and PlaceOrphans is the only place it is measured. A place nobody may take is absent from the row and is therefore a wall: a place already booked, a place in an area this configuration shuts, and a place an allotment holds for somebody else, which is occupied for this booker however empty it looks.

A place priced for another grade is the opposite case. Another booker may take it at another tariff, so it belongs to the run and keeps its neighbor company, and a party is never refused because the places beside it are somebody else's price.

Both statements are about ONE picture, and it is assembled in one place, PlacesInPlay: the hall, minus what the slot has booked, minus what an allotment holds away from this party, at every grade. The two enforcements below read that same picture. They used not to, and it cost bookings: the policy read the whole house and accepted takes that stranded a place beside a held section, while the seating read the party's own candidates and refused takes for holes the hall did not have. A strategy still keeps its candidate list, which answers the other question, which places may be given to this party.

A venue can also let a place somebody named themselves leave one on its own, which keeps the rule as a guide for everything below and stops it refusing a click. The rungs are unaffected either way: they weigh the rule because the alternative is proposing a run the hold would reject.

And it can put a party before a place. A row of ten cannot seat a party of nine legally, since taking nine strands the tenth; refusing that seats eight of them together and puts the ninth somewhere else, so a place is kept bookable by separating somebody from their party, and the place that ninth person takes may strand another. Keep a party together rather than leave a place on its own decides which the venue would rather have, and it is on by default. It is asked at one point only: after every arrangement the rule allows has been tried, and before the first rung that would split the party. While a legal seating exists it changes nothing at all.

It reads what a booking leaves, and refuses only what that booking creates. The stretches are measured before the take as well as after, so a booker is never refused for a hole somebody else left, and a hall that already has lone places does not stop being bookable the day the rule is attached. A place already on its own can always be taken, since taking the whole of a stretch leaves nothing behind.

What a strategy does about it

The policy refuses under the slot lock, which is where a place named on the map, by an operator or through the API is settled. That is not enough on its own: a strategy choosing without the rule would offer a run the hold then rejects, which is a dead end with a message nobody can act on. So the resolved number travels in the seating request, and every rung weighs it.

  • Extend and grow take one seat at a time, and weigh the rule against the whole of what is being asked for rather than against each seat as it is added. Two free places at the end of a row can be had together where neither can be had alone: taking the first would strand the second, taking both strands nothing. A seat that leaves a hole is therefore offered while places are still owed, and the finished take is what the rule judges, shrinking from the end until it passes. A party owed exactly one place is answered as it always was, and can still run out of room beside itself in a row that has places in it.
  • Re-place, adjacent and consolidate choose among the windows of a row instead of taking the first that fits, and take the first that leaves nothing on its own.
  • Near the party and spread hand a list to the share-out and weigh the answer, falling to the next rung when it strands a place.
  • Together is the exception, and it is the reason the order above matters. It is the same search as re-place or adjacent with the rule waived for one stretch, tried only once every legal arrangement has failed, and it stamps a flag of its own so the policy can permit what the seating deliberately chose. Not place_last_resort, which says there was nowhere better: this one says a place was paid to keep a party together, and an operator switches the two on and off separately.

Three things follow, and each of them is the rule working rather than failing. A row can hold enough places and still be unusable: four available with a party of three has no legal answer, so the party is seated elsewhere. A party that cannot grow where it sits is moved instead, by the re-place rung, which is why the rule is weighed on every rung and not only the first. And a house where nothing is legal seats the party anyway, on the last rung, marking the proposal as the last resort it is: the rule is a preference between arrangements, and where none is left to prefer, refusing would keep the place it protects and lose the party as well. The mark travels on the asks, so the rule allows exactly that proposal and no other. A venue that would rather keep the place switches the last resort off on the rule, and then the booking is refused. The figures a slot publishes are read from what is available, not from what this rule would accept, so the two can differ.

Giving a place back

Handing places back is never refused, but which place goes back can change. Clicking the middle of your own run on the map would leave that place empty between the two beside it, so a place at an extremity of the same run is given back instead, and the booker is told:

We gave back one of your end places instead, so yours stay side by side. A single empty place is rarely booked.

Where the booker is left holding one place there is nothing to sit beside, so the sentence does not pretend otherwise:

We gave back your other place instead. A single empty place is rarely booked.

Nothing moves and nobody is refused. The end state is the one that moving a neighbor into the hole would have reached, without the exchange: the run stays whole, one place shorter, and the place they clicked stays with the party. The substitution happens before the engine is called, so the release, the event it announces, the slot's own-writes record and the answer the map redraws from all describe the place that actually went back. It behaves exactly as though they had clicked that end themselves.

A run of clicks is one request, so the question is asked of the whole request: what the booker is left holding once every place in it has gone back. Places on their way out are not part of the run being kept together, and none of them is a place worth handing back instead, so giving back several at once gives back exactly those, and giving back a whole run empties it. The substitution is for the place left behind by a release, and a place that is itself leaving is never left behind.

How the map says it

Every message on the seat map goes through one path, whether it is a refusal or something done on the booker's behalf. There is no second mechanism and no dialog of its own: a message appears in the map's message area, is announced, and clears itself.

  • Type. Core's own three: status for something that happened, warning for a place that has gone, an area that is full, or a party the seating could not keep together, and error for a change that could not be saved. The type marks the message and colors it: a check for status, a bar and a dot for a warning, a circle with a diagonal stroke for an error, on green, orange and red. The mark rather than the color is what tells the three apart, because the three fills are nearly one darkness and read as one shade in grayscale, with colors disabled, or to a booker who cannot separate red from green. The shapes are the ones Claro uses, so a booker who also meets an admin screen reads the same three. The type also decides how it is announced: an error interrupts, everything else waits its turn. What the engine sends arrives carrying its own type rather than being given one here, so the plan and the price list say the same thing the same way.
  • Two channels, one sentence. The stack on the plan is what a booker reads; it is not a live region. Saying it out loud is Drupal.announce(), which every surface here goes through and which takes the urgency as an argument. A stack that was live as well would say everything twice to whoever is listening rather than looking, and a live region's urgency cannot be changed once assistive technology has registered it, which is why the type is not applied by moving a role around.
  • Life. Five seconds by default, and Keep a message on screen for at least on the booking settings screen changes it. The same number the price list keeps its own messages for, because how long a booker needs to read a sentence is the same question on the plan as it is on the list. Nothing waits on a message, so the number is a matter of taste rather than of correctness.
  • Stacking. A second message appears under the first rather than in its place. One answer often carries two: the party moved so that it could sit together, and how close together it managed. Saying something already on screen refreshes it rather than printing it twice.
  • Topics. Two messages about the same thing are one line, rewritten as soon as the later one arrives: the refresh control's result takes the place of its "checking for changes", and an area's count takes the place of the count before it. Everything else stacks.
  • Dismissal. Every message carries a button of its own to close it sooner, reachable by keyboard and named for what it does. Dismissing the one you have read leaves the one underneath that you have not.

The nearest end is preferred, so the booker keeps the places closest to the one they pointed at. Where no place of theirs can go back without leaving one on its own, which needs their run boxed in by other bookings and the row's edge, the click is honored as it stands: a booker unable to reduce a booking at all is a worse outcome than a hole.

A reduction driven by a quantity rather than by a click is unaffected, and the rules under "Reducing a party" above decide it.

Cost

None of this scales with the size of the hall. Placing reads the available places once for the whole party, whatever tariffs it asks for, and everything after that is arithmetic on that one list. Reducing reads nothing at all: which seats to hand back is answered entirely from the seats the booker is already holding.

The row map that arithmetic works from is built once for the whole ladder, not once per rung. Five rungs read the free places grouped into rows and all five read the same list, so grouping it per rung is a walk of the hall apiece for one answer (#3615214). The two rungs that fold the party's own places back in still build their own, because their list is genuinely a different one.

Seating a party in as few runs as it can be broken into reads nothing either. It groups what it was handed by row, by area and by section, which is the same arithmetic spreading a party does over the same list, and how long each run is comes out of a handful of sums over single-digit numbers. Nothing about it grows with the size of the hall, and it only runs at all once every rung above it has failed.

Closing a hole is the only one of the three that reads anything the booker is not already holding, because it has to know what the party could move onto. So it asks first whether there is a hole at all, and that question is answered from where the party sits: a party in one unbroken run is left alone before a single free place has been read. It is the ordinary answer, since a claim that split a party has already put it back together, and it is why the pass can be offered after every change without costing one.

Where there is a hole, the read earns itself back by answering from that one list: ranking every run in the house of the party's own length is a walk of the rows, and the party is checked against a set of the places it holds rather than searched per seat.

The matching in Mixed tariffs is the only part that is not linear, and it is skipped unless the requested tariffs price different grades.

"Once for the whole party" is counted rather than believed. AvailablePlaceReadCountTest puts a query log around a hold and counts the statements that read the seats: the sections a party could sit in are read, never a booking and never a tariff apiece, and the count does not grow with the size of the party. On the map path it reads none at all, since the visitor names the seat and nothing has to be proposed. The single read is not enforced by anything in the code, only by composing each slot once and by strategies working from the list they were handed, so the count is asserted to keep it that way.

What it reads is a slot's own seats, one row per seat, each saying whether it has gone. So the answer to "what is left in this section" is a seek on an index rather than the hall minus what has been taken, and it SHRINKS as the house fills rather than growing: the last click of a full house costs what the first one did. The rows are written when the slot is made and flipped as seats go and come back, which is why every path that takes or gives one back moves them, and why a test reconciles them against the bookings after each transition.

The sections themselves are asked for a page at a time, in the order an operator arranged the house, and the seating stops at the first section that can seat the party together. A section that could seat them only by scattering them is not an answer to stop on: another section may have a run long enough, and the pages turn until one does. What has been read accumulates, so a party too large for any one section is still seated across what the pages turned up, and where no page can seat them side by side the last one answers from everything read. A booker who named their seat or their area is answered with that part of the house and no more.

A booker adding to a basket is answered with the part of the house their party is already sitting in, so the newcomers land beside them and the hold waits on that part alone. Only where it can take the whole of what they asked for: where it cannot, they are answered with nothing in particular, and the seating fills the places beside the party first and puts the rest elsewhere, which is better than moving the newcomers away from their own party outright.

Writing your own strategy

Implement SeatingStrategyInterface and register it with the SeatingStrategy attribute. It has three methods: propose() for placing, sortForRelease() for reducing, and consolidate() for repairing. None of them is optional. A strategy that places well and reduces badly still breaks up parties, and one that never repairs leaves every hole open for good, since neither of the other two can close one. A strategy with nothing to say about repairing returns NULL, which leaves the booking alone; that is an answer, not an omission.

A strategy is handed everything it needs and reads nothing else, no storage, no request, no session state. That is deliberate. It is asked once per page for a whole party, and a strategy that queried per seat would put back the cost this design exists to remove.

Two services carry the parts that are nobody's opinion, and a strategy should ask them rather than work them out again (#3615214):

  • yoyaku_placement.place_geometry for the requirements about the hall: group places into rows, find a stretch of six free ones, find every window of that length, measure the unbroken stretches a row is left in, find the unbroken runs a party is sitting in, rank an area's rows, measure how far two places are apart.
  • yoyaku_placement.place_tariff_matcher for which of a run's places each tariff takes, matched properly where the tariffs price different grades.

Both take places and give back places or an assignment, hold no state between calls, and read no storage. Everything above them, the seven rungs, the two reducing passes and the way a hole is closed, is Together's own answer, and a strategy is free to disagree with all of it while measuring its hall the same way.