TLDR
(1) Printing full definition of FSM to console on application startup. (2) Working through additional test coverage of Action and Glide classes. A code smell (Glides having to duplicate information that could be found in their parent Action) motivates a refactoring of the FSM implementation.

Defining the transitions of a State.
public class StatePlaceFreeSettlement implements StateBase {
private static final StateEnum stateEnum = StateEnum.PLACE_FREE_SETTLEMENT;
private static final List<Transition> transitions = new ArrayList<>(List.of(
Transition.Builder.builder()
.setStartState(stateEnum)
.setAction(new PlaceSettlementAction())
.addGlide(new PlaceFreeSettlementToPlaceFreeRoadGlide())
.build()
));
.
.
.
}
Attempting to write a test for a particular Glide.
// NOTE: half-written test that pointed out weird design / code smell
@Test
public void givenValidCatanGame_whenValidateGlide_thenReturnTrue() {
catanGame.setStateEnum(StateEnum.ROLL_FOR_ORDER);
addPlayersWithPartialRolls(catanGame, 3, 2);
// NOTE: Issue is here - we always know a Glide's concrete action.
// NOTE: We shouldn't need to provide a mock.
IAction mockAction = createMockAction();
// TODO assert that game state is bumped to end state
assertThat(glide.validateGlide(catanGame, mockAction)).isTrue();
}
Issue is that Glide implements a method that takes the IAction interface as a parameter. We know that any particular Glide will always be passed a specific action (PlaceSettlementAction in this case). Reorganizing the FSM avoids boilerplate initialization of Glides and reduces the lift of writing tests and providing mocked values for IAction objects we always know the concrete implementation of.
This redesign also reduces the chance of errors, since we no longer would need to ensure the same IAction logic was being passed to both the parent Transition and its related IGlides.
@Override
public CatanGame performGlide(CatanGame catanGame, IAction action, Map<String, String> parameters)
Proposed solution is to remove StateBase class. FSM will be defined entirely by a list of Transitions, each of which have a starting StateEnum, ending StateEnum, and ActionEnum. If ActionEnum is something like ActionEnum.FREE, the Transition can happen automatically (e.g., player could not have made any moves). Concrete Transitions will implement an interface that allows for checking if they are relevant and possible to execute.
Main work
- Iterating over the FSM structure to print all possible transitions we’ve defined so far (e.g.,
StateEnum.ENTRYtoStateEnum.LOBBY).
Challenges
- It’s getting to the point where re-working the FSM logic isn’t very fun. It’s satisfying to delete “bad” code and get a cleaner solution, but it is a relatively big lift at this point.
- Especially considering this is a side project.
Learnings
-
At first, looked into the Spring Bean lifecycle events to try and run the FSM definition printing logic after all dependency injection has completed.
-
This lead to research on things like…
import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class MyBeanInitializer { @EventListener public void onApplicationEvent(ContextRefreshedEvent event) { // Logic to run after all beans are initialized System.out.println("All beans have been initialized via event listener!"); } } -
In the end, the
FiniteStateMachineis a singleton object, so I just called my function from the basic constructor.
-
-
Writing tests continues to force design reconsiderations / cleanup (both good things).
- I still do not feel that Test Driven Development from the start would have been reasonable. I needed to get SOMETHING FUNCTIONAL running before I could even start to consider edge cases or feel out how I want things to work.
- Maybe this is what is missed failed projects? Not enough time to build freely and eliminate the first 90% of bad ideas.
- I still do not feel that Test Driven Development from the start would have been reasonable. I needed to get SOMETHING FUNCTIONAL running before I could even start to consider edge cases or feel out how I want things to work.