Designing trading/shopping mechanics in ES style?

Hi,

I am prototyping a trading mechanic for my game and trying to do it in an ES-friendly way! I want to hear your idea about it and let me know if I am doing it the right way or if I can implement it in a different way.

I am going to have blacksmith, farmer, shopkeeper, miner,… NPCs in the village.

Blacksmith should be able to buy minerals from miners and turn them into weapons and farm tools.

Farmers should be able to buy farm tools from the blacksmith and buy farming seeds from seed sellers. They grow farm plants on the farm lands and sell the farm crops to market.

So NPCs need to be able to trade with each other and also with player.

I am creating an interface for them to be able to trade with each other.

public interface Trader {

    public enum OrderState {Success, UnAvailable, UnSupported, InsufficientBudget}

    public OrderState buyItem(String itemName, int amount, EntityId buyerId);

    public OrderState sellItem(EntityId itemId, int amount, EntityId sellerId);

    public OrderState orderItem(String itemName, int amount, EntityId orderedBy);

    public List<TradingItem> getItemCatalog();

    public boolean isItemAvailable(String itemName, int amount);

}

and

public record TradingItem(String name, Integer buyPrice, Integer sellPrice, int amount) {

    public boolean isBuyable() {
        return buyPrice != null;
    }

    public boolean isSalable() {
        return sellPrice != null;
    }
}

So blacksmiths, farmers, miners, and shopkeepers,… need to implement the Trader interface.

I also created a TraderType component that specifies which Trader an entity use.

public class TraderType implements EntityComponent, PersistentComponent {
    private int type;

    protected TraderType() {
        // For serialization
    }

    public TraderType(int type) {
        this.type = type;
    }

    public static TraderType create(String typeName, EntityData ed) {
        return new TraderType(ed.getStrings().getStringId(typeName, true));
    }

    public int getType() {
        return type;
    }

    public String getTypeName(EntityData ed) {
        return ed.getStrings().getString(type);
    }

    @Override
    public String toString() {
        return toString(null);
    }

    public String toString(EntityData ed) {
        return MoreObjects.toStringHelper(this)
                .add("type", ed != null ? getTypeName(ed) : getType())
                .toString();
    }
}

There is a TradingSystem that watches for entities that have a TraderType component and load the appropriate Trader type for an entity

public class TradingSystem extends AbstractGameSystem {

    private EntityData ed;
    private TraderContainer traders;
    private Function<String, Trader> loadFunction;

    public void setLoadFunction(Function<String, Trader> loadFunction) {
        this.loadFunction = loadFunction;
    }

    public Function<String, Trader> getLoadFunction() {
        return loadFunction;
    }

    public Trader getTrader(EntityId id) {
        if (!isInitialized()) {
            return null;
        }

        TraderWrapper wrapper = traders.getObject(id);
        if (wrapper != null) {
            return wrapper.getTrader();
        }

        return null;
    }

    @Override
    protected void initialize() {
        ed = getSystem(EntityData.class);
        traders = new TraderContainer(ed);
    }

    @Override
    protected void terminate() {

    }

    @Override
    public void start() {
        traders.start();
    }

    @Override
    public void stop() {
        traders.start();
    }

    @Override
    public void update(SimTime time) {
        traders.update();
    }

    protected Trader loadTrader(String type) {
        return loadFunction != null ? loadFunction.apply(type) : null;
    }

    private static class TraderWrapper {
        private Trader trader;

        public void setTrader(Trader trader) {
            this.trader = trader;
        }

        public Trader getTrader() {
            return trader;
        }
    }

    private class TraderContainer extends EntityContainer<TraderWrapper> {

        protected TraderContainer(EntityData ed) {
            super(ed, TraderType.class);
        }

        @Override
        protected TraderWrapper addObject(Entity e) {
            TraderWrapper wrapper = new TraderWrapper();
            updateObject(wrapper, e);
            return wrapper;
        }

        @Override
        protected void updateObject(TraderWrapper object, Entity e) {
            TraderType traderType = e.get(TraderType.class);
            Trader trader = loadTrader(traderType.getTypeName(ed));
            object.setTrader(trader);

            getSystem(GameObjects.class).ifPresent(e.getId(), o -> o.setVar(Trader.class, trader));
        }

        @Override
        protected void removeObject(TraderWrapper object, Entity e) {
            object.setTrader(null);

            getSystem(GameObjects.class).ifPresent(e.getId(), o -> o.removeVar(Trader.class));
        }
    }
}

So when a npc/player wants to trade something with another npc, they can get “Trader” object for that npc from “TradingSystem” and do the trading.

For player, there also will be a TradingHostedService & TradingClientService that manages the client-server communication for the player and also a TradingPanelState that visualizes the trading panel (i.e. display item list, prices,…) on a GUI.

What do you think? Am I doing it correctly in terms of ES? Do you have any suggestions for improving it?

Thanks in advance!

2 Likes

As far a “correctness” goes, I don’t see anything wrong.

From a design perspective, I have questions… more for you to ask yourself than anything specific. We tend to approach problems from a particular direction and follow a path without actually questioning how we got there and whether the trip was necessary. This happens to me all the time, especially as I adopt more “duck typing as a way of life” styles.

This was a very logical top-down break down of the problem. As developers/designers, it makes us feel good when we can put things into little “interface/class” boxes, think about subclasses, and so on. But sometimes (almost always) a bottom up approach is better if you can manage it. (Sometimes we don’t even understand the problem well enough for bottom-up design and so we make our little class and interface boxes to try to break down the problem.)

My first question is:
How do the “Trader” implementations actually differ?

Meaning: other than the stock they keep and some variables, is the “business logic” of a farmer really any different than a blacksmith? What are those differences?

Next question:
Is “TradingSystem” really only a fancy hash map? What is it providing that a lookup utility method would not?

There may be solid answers to these questions and I don’t mean to imply that there isn’t… but IF there isn’t then a bunch of these classes may be superfluous: just a product of thinking through a design. Sometimes once you’ve defined what the real “implementation” part is then the scaffolding can be removed.

An extreme example is the other direction where the things an NPC can/can’t do are just defined by the actions that it has. You/NPC can buy things from an NPC if they have a “buy” action and you can sell things to them if they have a “sell” action… and they could have one without the other, etc… Composition over inheritance.

A vending machine entity then might only be able to sell things. A recycling entity might offer a few coins for a “sell” action without having any “buy” action.

Just food for thought.

4 Likes

Thank you so much, it indeed helped me to make up my mind about this.

I do like the idea of using “actions” for trading. :slightly_smiling_face:

2 Likes