Dumb Game Lab

Blog · Behind the scenes

Dev log · behind the scenes

How We Built the Customer AI in PC Cafe Tycoon

Justin Lee, Dumb Game Lab ·

A PC Cafe Tycoon (PC방 키우기) game screen. Inside an isometric PC bang, a customer sits at each PC seat, and several customers walk along the sidewalk in front of the shop. A "Waiting 3" tag floats above the seat on the right

The customer logic in a tycoon game (usually called customer AI) works if you keep three rules. Assign seats as one tile per person, not as a number. Do not block tiles where a person stands: run A* pathfinding with a cost of 12 on them instead. Make customers wait in a single line outside the door and nowhere else. This post covers why we chose these three for PC Cafe Tycoon, the measured numbers, and what we have not solved yet.

Each customer arrives with an itinerary

When a customer spawns, they draw one itinerary. Each step of an itinerary means "use one fixture of a given type once". Because steps are written as a type name, the customer picks any one of the fixtures of that type, however many there are.

In a PC bang the itinerary is one step long. The customer sits down, leaves, and that is the whole trip, which makes it the simplest degenerate case of an itinerary. A game that gives no itinerary at all gets a one-step default: "use any fixture, then leave".

There is one safeguard. If the fixture for the next step does not exist in the shop at all, that step is skipped. A customer never freezes in place waiting for something you have not built yet.

A seat is a tile, not a number

At first it looks easier to give each fixture a capacity number. But with a capacity number, eight customers can end up standing on the same coordinate. A number only knows how many people use a fixture, not who stands where.

So each fixture gets an interaction tile (interactionCell), and only one customer can occupy a tile. The moment a customer chooses a target fixture, they reserve that tile, and no other customer can take it. You no longer need to write a capacity anywhere, and overlap cannot happen by construction.

We also do not pick targets with equal odds. If we did, a vending machine right next to the seats and one on the far side of the room would sell the same amount, and revenue would be the same wherever you put things. Weights are what make layout affect revenue.

A PC Cafe Tycoon game screen. One customer sits in each chair at the PC desks, with a counter and a potted plant in the middle
One tile per PC, one customer per tile.

Do not block tiles where people stand: give them a cost

Paths come from 4-direction grid A*. It asks separate questions: can you stand on this tile, can you cross it, can you move between these two tiles, and finally what does it cost to pass through. A tile with a seated customer costs 12.

Turning people into walls would be simpler. But this game's placement check guarantees the room is never cut apart on the assumption that "you can also walk through the tile in front of a fixture". Once people become walls, that assumption breaks, customers starve in a narrow room, and that day's revenue drops to 0.

With a cost, customers always take a detour when one exists and squeeze through only when there is truly no way around. The squeezing looks odd, but it is a signal that the layout left no aisle. It is not an engine bug.

A path is not laid once and finished

We thought adding the cost of 12 would settle it, but it barely fixed anything. The cause was stale paths. If someone sits down on a route after it was computed at departure, the cost exists but the customer keeps walking the old route.

So the people map is rebuilt from scratch every tick. Maintaining it incrementally means one code path forgets to clear a tile, and a ghost blocks an aisle forever. A walking customer rechecks the next tile every tick, and if it is blocked, the path is recomputed on the spot. That is once every 0.2 seconds per customer.

If the recomputed path still goes through that tile, the customer just goes. Stopping them would make customers wait on each other and freeze.

In a dense layout we ran 40 seconds and counted frames where a customer stepped onto a standing person. Before adding the cost it was 1,701. With the cost but no recomputation it was 1,695, almost unchanged. Adding only the cost without recomputing fixes almost nothing. The real cause was stale paths.

A PC Cafe Tycoon game screen. Four customers walk along the sidewalk in front of the shop, and inside the shop above, customers sit at PC seats
A walking customer rechecks the next tile every tick.

Waiting is a single line outside the door

If you make customers wait inside when seats are full, the line blocks the aisles and pathfinding tangles. So waiting is always a single line outside the door. A customer who has not entered yet joins the outside line. A customer who is already inside does not queue and gives up that step and leaves. Whether a seat is free is checked at the yard tile in front of the door. It used to be checked at the entrance tile, so when the shop was full, customers opened the door, stepped in, and turned back out.

The line is not unlimited either. Its capacity is the smaller of the number of tiles available for standing and the cap the game sets (4 by default). Past the capacity, a customer does not join and turns away. If a customer finds out on the way that the line is already full, they turn around at the sidewalk next to the tail.

The important part is not to delete the customers we could not serve in silence. A customer who gives up plays a give-up animation and shows a popup, and the game tallies "which type, and for what reason we lost them". If the bottleneck is not visible on screen, the player has no reason to think "let's put in one more PC". They would not even see that customers are leaving.

A PC Cafe Tycoon game screen. A customer sits at a PC seat, and a "Waiting 3" tag floats above the seat on the right. A +2,135 won revenue popup is visible on the left
The "Waiting 3" tag lets you read where the bottleneck is straight from the screen.

Everyone in the line faces the same way

After we added the line, a player report came in (September 19, 2026): "I'd like everyone to face the same direction while waiting." We checked, and facing was only being set while walking. A customer coming up from the sidewalk faced the wall, one who walked to the tail faced the tail, and one who was pulled up a tile faced the head of the line. Three directions were mixed in one line.

The fix is simple. When a customer reaches their place in the line, every customer in the line is set to face the head of the line. The outside line runs straight along the wall, so everyone faces the same way. The rule we took from this is that every place a customer stops needs a defined facing.

We also made customers hold still in the line. We tried shaking the body to show impatience, and got two reactions: "it looks like floating" and "it's trembling by itself". Now only a breathing animation plays.

In a PC bang, 80% of revenue comes after they sit down

The PC bang is one step in the itinerary, but revenue does not end there. A step means "a place you visit once", so it cannot repeat. A seated customer ordering something extra now and then does not happen in the itinerary. It happens in an in-use hook. Those orders make up 80% of daily revenue.

So customer logic is tied not only to how fast seats fill, but to how long a seated customer stays before ordering. For the same reason there is an option that keeps a customer in the seat after their stay time is up if what they ordered has not arrived.

What we have not solved yet

Walking customers still overlap briefly. Pathfinding treats a person as a cost only when they have taken a place and stopped (customers in use and the inner part of the line). Walking people are not put on the map. If walking people avoided each other, you would get a deadlock in a narrow aisle where two customers face each other and circle. They leave that tile within a few frames anyway.

So frames where walking customers overlap are treated as normal by the rules. Fixing it properly would mean designing a yield order, not a sidestep, and that is out of scope for now.

FAQ

How do you build customer AI for a tycoon game?

Give each customer an itinerary, reserve the interaction tile of the target fixture one customer at a time, and find paths with A*. Tiles where a person stands are not blocked but given a high cost (12 here), and when there is no seat, the customer waits in a single line outside the door.

How do you keep NPCs from blocking each other in a tycoon game?

Do not turn standing people into walls. Add them as a cost, and keep recomputing paths so that cost is reflected. Adding the cost without recomputing barely helped. In a 40-second measurement on a dense layout, frames stepping onto a standing person only went from 1,701 to 1,695.

Should the waiting line be inside or outside the shop?

PC Cafe Tycoon uses only a single line outside the door. Inside, it blocks the aisles and tangles pathfinding. A customer who is already inside does not queue and gives up that step, and the game tallies every give-up and shows it on screen.

Is it fine to just delete a customer who leaves without a seat?

No. Show a give-up animation and a popup and tally them by type. That is what lets the player read the bottleneck and decide "let's put in one more PC".

More from the blog

New games and small updates, about once a month.

Read inEnglish한국어日本語繁體中文