TLDR
Improving the automated testing setup I used for local development. Implement checks in CatanBankService that ensure a player can make particular purchases. Checking which purchases a player can make before returning available Actions.
if is_manual_execution:
# ask for user confirmation
user_input = input("Do you want to execute this command? (y/n): ").strip().lower()
# default to "yes" if input is empty
if user_input == '' or user_input == 'y':
os.system(bash_command)
elif user_input == 'n':
print("Skipping command.")
else:
print("Invalid input. Skipping command.")

Avoiding manually confirming 20+ commands to get board into proper testing state.
public static boolean isDevCardPurchasePossible(CatanGame catanGame, int userId) {
int totalDevCardsAvailable = CatanGame.DEV_CARD_COUNTS
.values()
.stream()
.mapToInt(Integer::intValue)
.sum();
long totalDevCardsPurchased = catanGame
.getPlayers()
.stream()
.map(Player::getDevCards)
.mapToLong(List::size)
.sum();
boolean isDevCardAvailable = totalDevCardsPurchased < totalDevCardsAvailable;
boolean doesPlayerHaveSufficientResources = catanGame
.getPlayerByUserId(userId)
.getResources()
.isGreaterThanOrEqualTo(CatanGame.REQUIREMENTS_DEV_CARD);
return isDevCardAvailable && doesPlayerHaveSufficientResources;
}

Showing the available purchase Actions during TURN_SPEND_RESOURCES state.
Main work
- Update the automated Python script to accept an “automatic” flag (no user validation for commands).
- Introducing
PlayerDevCardstructure to represent played or unplayed development cards for each player. - Updating
CatanBankServiceto check if total resources have run out (e.g., a player has run out of road pieces to place). - Adding a
/clearHTTP endpoint to use during automated testing and reset game state. - Scaffolding actions for purchasing items, such as
FourForOneTradeAction. - Updating
TURN_SPEND_RESOURCESstate to compute the available purchases (actions) that the user can make (viaCatanBankService).
Challenges
- The testing loop is very manual:
- The server restarts.
- The automated testing script is executed to setup initial board state.
- I manually take some action via the UI.
- I confirm UI changes look correct.
- I confirm debugger values for backend server look correct.
- The testing loop was meant to be a quick and dirty way to build some momentum, but it is the primary way I do my development now.
- I have very limited and short time windows to make progress, so I have prioritized getting more features done rather than reworking the test harness.
- The verbosity of Java is tiring. There are many classes that need to be written with slightly different logic and very similar boilerplate.
Learnings
- A “quick and dirty solution” can exist for much longer than intended.
- This manual testing workflow can be improved by writing proper unit / integration tests.
- Alternatively, an agent like Claude Code could run the tests on my behalf.
- The agent could leverage MCP servers like Puppeteer to check the final UI state.
- The awkward syntax / Java quirk explained below.
Why Is new ActionEnum[0] Needed Here?
Consider the setAvailableActions method and its usage.
// call to method, requiring array type with "strange" parameter
catanGame.getActivePlayer().setAvailableActions(
availablePurchaseActions.toArray(new ActionEnum[0])
);
// method signature, taking variable num of args
public void setAvailableActions(ActionEnum... availableActions) {
this.availableActions.clear();
Collections.addAll(this.availableActions, availableActions);
}
- We want to provide an array to the
setAvailableActionsmethod, but currently have aList<ActionEnum>.ActionEnum... availableActionsis equivalent toActionEnum[] availableActions
- The
toArraymethod has signture<T> T[] toArray(T[] a). - Java has type erasure for generics, meaning that the
toArraymethod does not know the specific type ofTat runtime.availablePurchaseActionsis only aListtype at runtime, not aList<ActionEnum>type.
- To get around this type erasure, we provide a zero-memory-allocation instance of the concrete type.
- This concrete instance survives at runtime, and provides the necessary type information.
- More modern java would use a method reference like
availablePurchaseActions.toArray(ActionEnum[]::new).