Wedding Seating Chart: Best-Fit Placement, RSVP Changes and Near-Preferences
MediumAI-assisted coding · AI-Assisted Feature Work
Wedding Seating Chart: Best-Fit Placement, RSVP Changes and Near-Preferences
You've joined the team behind an event-planning app. Couples enter their tables and guest list, press Plan, and get a seating chart. The current planner hands out seats first-come-first-served, and planners are unhappy: big families get split up in spirit (stuck at whatever is left), exes end up at the same table, and the head table fills with random guests. Three tickets are queued.
Codebase
| File | What it does |
|---|---|
seating/api.py | run(operations) entry point and the Dispatcher that routes each operation |
seating/service.py | PlannerService: argument validation, setup operations, plan, seating, rsvp |
seating/placement.py | choose_table (the decision for one party) and place_all (builds a whole plan) |
seating/plan.py | SeatingPlan (who sits where, seats used per table) and describe_plan (the wire form) |
seating/guestbook.py | GuestBook: parties, the keep-apart relation, and the placement order |
seating/venue.py | Venue: tables and capacities |
seating/rsvp.py | apply_rsvp stub for Part 2 |
seating/chart.py | render_chart and near_score stubs for Part 3 |
seating/audit.py | audit_plan: checks a plan against its invariants (handy for tests and debugging) |
seating/models.py, errors.py, config.py | Records, error codes, constants |
tests/ | Existing unit and end-to-end tests |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh event and returns one result per operation. Every operation is [name, *args]. Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments | Returns | Errors |
|---|---|---|---|
add_table | number, capacity, head | true | table exists |
add_party | party_id, size, rsvp, head | true | party exists |
add_party_run | prefix, first_rsvp, sizes | number of parties added | party exists |
keep_apart | party_a, party_b | true | same party, unknown party |
plan | none | the plan (see below) | none |
- A party is a group of guests (
size >= 1) that must sit together at one table.rsvpis the integer time the party replied (>= 0, smaller = earlier).headis a boolean. A table has an integernumber >= 1, an integercapacity >= 1and aheadboolean. Booleans are never accepted where an integer is expected, andheadmust be a real boolean. - Ids are case-sensitive non-empty strings, compared with ordinary string order.
add_party_runis the spreadsheet import:sizesis a non-empty list of integers>= 1, and rowi(counting from 1) becomes the non-head party<prefix><i>withrsvp = first_rsvp + i - 1. It is all-or-nothing: if any of those ids already exists, nothing is added.keep_apartis symmetric: the two parties must never sit at the same table. It is not transitive (a/bapart andb/capart say nothing aboutaandc). Repeating a pair is fine.- Check order for every setup operation: wrong argument count, types or ranges returns
invalid arguments; thenplan locked(see below); then the operation's own errors in the order listed in the table (forkeep_apart,same partyis checked beforeunknown party). An unknown operation name returnsunknown operation. An operation that is not a non-empty list starting with a string returnsinvalid arguments. planfreezes the event: after the firstplan,add_table,add_partyandkeep_apart(andprefer_nearin Part 3) returnplan locked. Callingplanagain is allowed.- The plan is
{"tables": [[number, [party ids]], ...], "unseated": [party ids]}. Tables are listed by number ascending and every table appears, even an empty one. Party ids within a table are ordered by size descending, then id.unseatedis ordered by size descending, thenrsvp, then id.
Worked example
[["add_table", 1, 6, false], ["add_table", 2, 6, false],
["add_party", "aoki", 2, 1, false], ["add_party", "bell", 4, 2, false], ["add_party", "cruz", 3, 3, false],
["keep_apart", "aoki", "bell"],
["plan"]]The last result is {"tables": [[1, ["bell"]], [2, ["cruz", "aoki"]]], "unseated": []}.
Part 1 — Replace the first-fit planner (PLAN-412)
Goal: make plan use the placement algorithm below instead of "first table with room, in RSVP order". Parties and tables are given by the setup operations; plan computes where everybody sits.
Rules
- Order. Parties are placed one at a time sorted by
sizedescending, thenrsvpascending, thenidascending. - Eligible tables for a party: (a) the table's
headflag equals the party'sheadflag (head parties sit only at head tables and nobody else may use a head table); (b) at leastsizefree seats; (c) no party already seated there is kept apart from it. - Best fit. Among eligible tables choose the one with the fewest free seats (before the party sits down); ties go to the lowest table number.
- A party with no eligible table is unseated. It is skipped: smaller parties after it are still placed normally, and nobody is moved to make room.
planalways works from the current guest list and tables alone: calling it twice in a row returns the same result. Parties are placed whole, never split.
Example (the worked example above): order is bell (4), cruz (3), aoki (2). Both tables have 6 free, so bell takes table 1. cruz needs 3: table 1 has 2 free, so table 2. aoki fits table 1 (2 free) but bell must stay apart from her, so she joins cruz at table 2.
Part 2 — RSVP changes without reshuffling (PLAN-437)
Goal: guests change their minds after the chart is printed. Add rsvp so a size change is applied to the existing plan with as few moves as possible: nobody who is seated and still fits ever moves. (Re-running the whole placement would reshuffle tables people have already been told about.)
| Operation | Arguments | Returns | Errors (in order) |
|---|---|---|---|
seating | none | the current plan, same shape as plan | no plan |
rsvp | party_id, new_size | list of moves | invalid arguments, unknown party, no plan |
new_size is an integer >= 0; 0 means the party declined. seating returns the plan as it stands now (after any rsvp calls). plan always recomputes everything from the current party sizes and is the only operation that may move everyone.
Rules for rsvp
- The party's size becomes
new_size. A declined party (size 0) is not seated and does not appear inunseated; a laterrsvpwith a positive size brings it back. - If the party is seated and
new_size > 0: it stays at its table if the table can hold the new size alongside the others already there (this covers shrinking, and growing into free seats). Otherwise it is taken off its table and needs a new one. Ifnew_sizeis 0 it is simply removed. - Afterwards, every unseated party (the one that just lost its table included) is offered a table in the Part 1 order (size descending, then
rsvp, then id), using the Part 1 table choice (eligibility and best fit) against the plan as it currently stands. Anyone who finds a table sits there; anyone who does not stays unseated. Nobody who is seated is ever moved to make room. - The result lists one move
[party_id, from_table, to_table]per party whose table changed (nullfor "not seated"). The changed party comes first (if its table changed), then the other parties that got a seat, in the order they were seated. No change returns[].
Example (tables 1 and 2 hold 6, table 3 holds 4; parties a 4, b 3, c 3, d 2, e 2, f 1 with rsvp 1 to 6 in that order, none head, no keep-apart). The plan is T1 [b, c], T2 [d, e, f], T3 [a].
| Operation | Result |
|---|---|
["rsvp", "e", 4] | [["e", 2, null]] (table 2 has only 3 free for the 4 guests; nowhere else fits) |
["rsvp", "c", 1] | [] (shrinks in place; the 4 guests of e still do not fit anywhere) |
["rsvp", "a", 0] | [["a", 3, null], ["e", null, 3]] (a declines; e takes table 3) |
Afterwards seating returns {"tables": [[1, ["b", "c"]], [2, ["d", "f"]], [3, ["e"]]], "unseated": []}.
Part 3 — "Seat us together" and the printable chart (PLAN-455)
Goal: couples want to say "these two parties should share a table if possible". Add soft preferences that influence placement without ever breaking a Part 1 rule, plus a chart planners can print.
| Operation | Arguments | Returns | Errors |
|---|---|---|---|
prefer_near | party_a, party_b, weight | true | same checks and order as keep_apart |
near_score | none | [satisfied, total] | no plan |
chart | none | list of strings | no plan |
weight is an integer from 1 to 10 (anything else is invalid arguments). A pair may be given again; the latest weight wins. Preferences are symmetric. A party with at least one preference is called *linked*; the rest are unlinked.
Rules
- Table choice. For a party, each eligible table gets an *affinity*: the sum of the weights of the party's near-partners that are currently seated at that table. Choose the eligible table with the highest affinity, then fewest free seats, then lowest number. For a party with no seated partner every affinity is 0, so this is exactly the Part 1 rule. Eligibility never changes.
- Pulling partners forward (
planonly). Parties are still offered tables in the Part 1 order, but after a party has been placed (or found unseatable) its *unplaced* near-partners with positive size are placed immediately next, highest weight first (ties: Part 1 order), and each of them pulls its own unplaced partners in turn, before the next partner of the first party is handled (depth first). Then the walk resumes with the next unplaced party in Part 1 order. Unlinked parties are never pulled and keep their place in the order. rsvpre-placements (Part 2, rule 3) use the new table choice (rule 1) but never pull partners.near_scoreis[S, T]:Tis the sum of the weights of all preferences,Sthe sum over those whose two parties are seated at the same table right now.- Chart.
chartreturns one line per table by number ascending, then anUnseatedline, then a score line:
Table <n> <used>/<capacity>: <parties>, orTable <n> (head) <used>/<capacity>: ...for head tables.<parties>is the table's parties ordered by size descending then id, each written<id> x<size>, joined by,; an empty table prints-.Unseated: <parties>, inunseatedorder, each<id> x<size>;-when nobody is unseated.Near-score: <S>/<T>.
Example (tables 1 and 2 hold 6, table 3 holds 4; lee 4 rsvp 1, kim 3 rsvp 2, ono 2 rsvp 3, pak 2 rsvp 4; ["prefer_near", "pak", "kim", 5]; then plan, chart, near_score): lee takes table 3 (4 free is the tightest fit). kim takes table 1, and then pak is pulled forward ahead of ono: table 1 has affinity 5 for pak and room, so she sits with kim. ono goes to table 2.
["Table 1 5/6: kim x3, pak x2", "Table 2 2/6: ono x2", "Table 3 4/4: lee x4", "Unseated: -", "Near-score: 5/5"]and near_score returns [5, 5]. Without the preference pak would have been placed last, in table 2.
Example 1
- Input:
- [["add_table",1,6,false],["add_table",2,6,false],["add_party","aoki",2,1,false],["add_party","bell",4,2,false],["add_party","cruz",3,3,false],["keep_apart","aoki","bell"],["plan"]]
- Output:
- [true,true,true,true,true,true,{"tables":[[1,["bell"]],[2,["cruz","aoki"]]],"unseated":[]}]
- Explanation
- Parties are placed largest first. bell takes table 1 (tie on free seats goes to the lower number), cruz needs 3 so she takes table 2, and aoki must stay away from bell.
Example 2
- Input:
- [...tables 1,2 (6 seats), 3 (4 seats); parties a4 b3 c3 d2 e2 f1..., ["plan"],["rsvp","e",4],["rsvp","c",1],["rsvp","a",0]]
- Output:
- [..., [["e",2,null]], [], [["a",3,null],["e",null,3]]]
- Explanation
- e outgrows table 2 and fits nowhere, so it is unseated. c shrinks in place. When a declines, e takes the freed table 3. Nobody else moves.
Example 3
- Input:
- [...tables 6,6,4; lee 4, kim 3, ono 2, pak 2; ["prefer_near","pak","kim",5],["plan"],["chart"]]
- Output:
- [..., ["Table 1 5/6: kim x3, pak x2","Table 2 2/6: ono x2","Table 3 4/4: lee x4","Unseated: -","Near-score: 5/5"]]
- Explanation
- pak is pulled forward right after kim is seated, so she joins kim's table instead of being placed last.
Guidelines
- Python 3 standard library only
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Output must be deterministic: no dependence on dict or set iteration order
- Part 2 inputs contain hundreds of RSVP changes against a plan with many parties and keep-apart pairs