Law-abiding decks
Topics: validation,
Raise, error accumulation
Handling errors is one of the scenarios where a functional approach shines. Using types like Either and contexts like Raise, we can easily compose larger validations from smaller ones.
This topic is well documented in the official Arrow documentation, we suggest the reader to check the following material:
Feel free to use any style that you prefer in this section. When in doubt, using Raise (as opposed to Either) is the preferred option when using Arrow.
Legal decks
Your task in this section is to implement the rules for a legal deck, that is, one that can be used to play Pokémon TCG. The tcg/validation.kt file contains a barebones implementation of validate, which simply checks the number of cards in the deck, and a non-empty title.
In the given implementation of deck/viewModel.kt you may have noticed that problems has quite a strange type, namely NonEmptyList<String>?. This is equivalent to a simple List<String> if you consider "null = empty", so why bother with such a complication? Doing so forces us to handle the null case (no errors) explicitly, whereas with a List<String> we have no such guarantee: you may end up inadvertently showing that the deck has problems, but then showing none. The goal here is to make the compiler catch potential problems before they even arise.
The module.yaml file enables the (experimental) return value checker. This ensures that every Either you obtain is ultimately consumed; since otherwise you may lose some valuable errors along the way. To get yourself acquainted with this feature, try to remove some .bind() calls from the code you wrote for the previous tasks, and watch the compiler complaining.
Main rules
The main rules for the legality of a deck are:
- The deck must contain exactly 60 cards,
- There must be at most 4 cards with the same name,
- Note that cards with different identifiers but the same name are added together,
- The only exception to this rule are basic Energy cards, of which you can have an unlimited amount,
- There must be at least one basic Pokémon.
Implement this validation using Raise or Either, and try to break the process in different functions. The notion of fail-first vs. accumulation is important here, so you can squeeze as much information as possible. The skeleton for the function is already given: you should write the body of either validateRaiseImpl or validateEitherImpl, and update validate to call the corresponding function.
Better (error) types
The current implementation goes against our aim of precise types, since it uses String to represent any problem. Your task here is to introduce an error hierarchy that represents each possible problem with the deck. More concretely:
- Introduce a new
DeckProblemsealed hierarchy, - Introduce as many subclasses of
DeckProblemas kinds of rules that may be broken, including in each of them any additional information that could be useful while reporting, - Modify the
validatefunction to return aNonEmptyList<DeckProblem>?, and update the code to build instances of those errors instead of using strings.
As a result of this transformation the DeckPane view cannot directly print the errors, as it was doing until now. You need to define how each DeckProblem should be represented as a message.
Problems tied to specific cards
Some problems pertain the whole deck (like having too few or too many cards), whereas others refer to specific cards (like having more than 4 cards) with the same name. We want the UI to show those problems differently; for example, by showing the name in the MaterialTheme.colorScheme.error color. But there may be some information missing to tie errors to specific cards. Your task is to refine your error hierarchy to account for this information, and update the UI accordingly.
This task is especially interesting to understand the different roles of view models and views when representing information. During validation it makes sense to account for errors in a uniform fashion, whereas in the UI we want them to appear separately. By introducing a good error hierarchy, making this distinction becomes easy.
Evolution
Implement a rule to check that you can always evolve every Pokémon in your deck. This means you if you have a Stage 1 or Stage 2 Pokémon, you should have a card for the Pokémon it evolves from.
Gym Leader Challenge
The rules described above correspond to the Standard format, which is the one sanctioned for tournaments. However, fans of the game have come with other formats, like Gym Leader Challenge (GLC). As an extra task, you may implement GLC rules.
- You may need to add some UI element to specify the format your deck is in.
- GLC forbids some sorts of cards, namely those with a Rule Box and ACE SPECs. This information is available from the API, but currently not reflected in the domain model.
Property-based testing
One big advantage of following an immutable approach to modelling decks is that testing becomes much easier, since there are no dependencies to account for. In particular you can use property-based testing, an approach in which lots of random inputs are generated, and then properties of the result are checked. This raises the level of abstraction: for example, you don't test that a certain deck returns certain error, but rather that "validating every deck with fewer than 60 card contains a NotEnoughCards error among those returned".
Your task is to add more tests to the test/tcg/validation.kt file. This file already contains one test you can use as a template. The project is set up to use Kotest, in particular using the checkAll function. For more complex scenarios yoy may need to write custom generators that provide more exact input to your tests.
Reactive problems
The current implementation has a potential problem: you need to update problems every time you update deck. But actually, the problems of a deck directly derive from the contents of the deck itself. Reactive frameworks like RxJava allow expressing this connection directly, and we can easily do the same using a StateFlow.
Your task is to replace each update to the problems mutable state with a new definition based on deck. You can use the function map in utils/flow.kt.