At its core, Project Kuiper is a card-based game, where the player draws cards from a deck and plays them to take actions. These actions include building new facilities, dealing with crises, running special research projects, and more. I've been working on various core mechanics, and it was time to start implementing the actions.

There will be a lot of actions, but I have started with actions which change (or mutate) some of the core resources for the player - money, influence, construction materials, and the sciences. Actions may take several turns to complete, and actions may have a cost in resources to play. Actions may have an effect for each turn that they are active, or they may have a one-off effect. Or both.

The player will play an action by placing it on a tile on the board, and it will be added to the list of 'active actions'. For each turn of the action's duration, the mutations will execute. A mutation may boost the research rate of a particular science, but at the cost of money per turn.

My Action class looks a little like this:

class Action(val id: Int, val name: String, val description: String, var duration: Int = 1) {
    var turnsRemaining: Int = duration
    private val actionCosts: MutableMap<ResourceType, Int> = mutableMapOf(
        ResourceType.GOLD to 0, ResourceType.INFLUENCE to 0, ResourceType.CONSTRUCTION_MATERIALS to 0
    )
    private var propertyToMutate: ResourceType =
        ResourceType.GOLD
    private var mutationEffect: MutationType = MutationType.ADD
    private var amountPerYear: Int = 0
    private var completionAmount: Int? = null
    private var scienceToMutate: Science? = null
    private var scienceRateAmount: Float = 0.0f
}

Then there are the Mutations - mutations will either add a value to a resource (or subtract, with a negative value), change a value to a specific amount, or multiply a value by a specific amount. Others will need to be added as the game progresses - build, upgrade, destroy, bribe, special project, etc.

interface Mutation {
    var type: MutationType
}
class ScienceMutation(
    val science: Science, override var type: MutationType, val amount: Float
) : Mutation {
    override fun toString(): String {
        return "$type $science by $amount"
    }
}
class ResourceMutation(
    val resource: ResourceType,
    override var type: MutationType,
    val amountPerYear: Int,
    val completionAmount: Int? = null
) : Mutation {
    override fun toString(): String {
        return "$type $resource by $amountPerYear pa, to $completionAmount"
    }
}
enum class MutationType {
    ADD, SET, RATE_MULTIPLY
}

I have different mutations for resources and sciences, as they are handled differently in the game. The Action class will have a list of Mutation objects, which will be executed.

To create an Action, I will eventually define them in a JSON file. But they can also be created in code, like so:

val action_invest_gold_for_five_turns =
        Action(5, "Invest gold", "Spend 5 gold for 5 turns, receive 50 gold reward", duration = 5)
    action_invest_gold_for_five_turns.addMutation(ResourceType.GOLD, MutationType.ADD, -5, 50)

This example could represent "investing in the stock market", for instance. I'd like to be able to give mutations randomised values, so that the player can't predict the outcome of an action. The investment may not pay off, or it may pay off handsomely. THat's for a later version though.

I build all of this without adding any UI or really interacting with Godot at all. I've got a main() function, which builds a set of Actions, adds them to the core Company class, and then calls company.nexTurn() to trigger them. This is working, but incomplete and will need further refinement.

Now that I have implemented Actions and Research, I have two of the three core components in a proof-of-concept form. Before I start on the third (the game 'map' and location hexes), I've started to integrate Actions and Research into the Godot UI. Unfortunately, my holiday is coming to and end, and I'd better get back to the day job...