A menu tree, and games that can be in more than one branch of it
Three sections on the main screen -- Solitaire, Card Games, Board Games -- and Trick-taking sitting under Card Games rather than beside it, because it is a kind of card game and not a rival to one. GameDescriptor carries a set of menus rather than one category. A patience is solitaire and a card game; Euchre is a card game and a trick-taking one; peg solitaire is solitaire and a board game. Each is listed in every place it belongs, which is the point: a game reachable one way and not the other is a game somebody cannot find. GameMenu reads its parent from a `when` rather than taking it as a constructor argument. An enum entry cannot refer to another entry while the class is still initialising, and the lookup afterwards costs nothing. Counting a section counts a set rather than adding lists together, since a game in both Card Games and Trick-taking is one game and not two. hasAnything asks about a menu and its children together, so a section is only hidden when there is nothing under it anywhere -- the same rule that used to keep a heading from being stranded over an empty category. The dev-build "coming soon" behaviour is unchanged and still tested. What could not survive the change is the old test that counted games across categories and expected the total to equal the catalogue: with a game in several menus that sum is meaningly larger. It is now a union, which is what it was really checking -- that no game is stranded in no menu at all. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC
This commit is contained in:
@@ -16,21 +16,54 @@ class CatalogTest {
|
||||
|
||||
@Test
|
||||
fun `a production build lists only games that can be played`() {
|
||||
GameCategory.entries.forEach { category ->
|
||||
val shown = GameCatalog.byCategory(category, includeComingSoon = false)
|
||||
GameMenu.entries.forEach { menu ->
|
||||
val shown = GameCatalog.inMenu(menu, includeComingSoon = false)
|
||||
assertTrue(
|
||||
"$category offers an unfinished game in a release build",
|
||||
"$menu offers an unfinished game in a release build",
|
||||
shown.all { it.available },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a dev build lists the whole catalogue`() {
|
||||
val shown = GameCategory.entries.flatMap {
|
||||
GameCatalog.byCategory(it, includeComingSoon = true)
|
||||
fun `every game is reachable from at least one menu`() {
|
||||
// Counting is no good now that a game can be in several menus: the
|
||||
// union is what matters, and a game in none of them would be in the
|
||||
// catalogue but on no screen.
|
||||
val reachable = GameMenu.entries.flatMap {
|
||||
GameCatalog.inMenu(it, includeComingSoon = true)
|
||||
}.toSet()
|
||||
assertEquals(GameCatalog.all.toSet(), reachable)
|
||||
GameCatalog.all.forEach {
|
||||
assertTrue("${it.id} is in no menu", it.menus.isNotEmpty())
|
||||
}
|
||||
assertEquals(GameCatalog.all.size, shown.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a game can be in more than one menu`() {
|
||||
// The whole point of the set. A patience is solitaire and a card game;
|
||||
// Euchre is a card game and a trick-taking one.
|
||||
val klondike = GameCatalog.byId(GameCatalog.KLONDIKE)!!
|
||||
assertTrue(klondike.menus.containsAll(setOf(GameMenu.SOLITAIRE, GameMenu.CARDS)))
|
||||
assertTrue(GameCatalog.inMenu(GameMenu.SOLITAIRE).contains(klondike))
|
||||
assertTrue(GameCatalog.inMenu(GameMenu.CARDS).contains(klondike))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `trick-taking sits under cards, and its games are card games too`() {
|
||||
assertEquals(GameMenu.CARDS, GameMenu.TRICK_TAKING.parent)
|
||||
assertEquals(listOf(GameMenu.TRICK_TAKING), GameMenu.CARDS.children)
|
||||
assertTrue(GameMenu.TRICK_TAKING.isTopLevel.not())
|
||||
assertTrue(GameMenu.SOLITAIRE.isTopLevel)
|
||||
|
||||
val tricks = GameCatalog.inMenu(GameMenu.TRICK_TAKING)
|
||||
assertTrue("there should be some", tricks.isNotEmpty())
|
||||
// A sub-menu that listed something its parent did not would be a game
|
||||
// you could reach one way and not the other.
|
||||
assertTrue(
|
||||
"every trick-taking game is also a card game",
|
||||
tricks.all { it.menus.contains(GameMenu.CARDS) },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -42,19 +75,19 @@ class CatalogTest {
|
||||
// engine is written.
|
||||
assertTrue(GameCatalog.all.all { it.available })
|
||||
assertEquals(
|
||||
GameCatalog.byCategory(GameCategory.BOARD, includeComingSoon = false),
|
||||
GameCatalog.byCategory(GameCategory.BOARD, includeComingSoon = true),
|
||||
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = false),
|
||||
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hiding unfinished games still leaves something to play in each category`() {
|
||||
// A category filtered down to nothing would leave a stranded heading,
|
||||
// and a menu with no playable game at all would be a broken release.
|
||||
GameCategory.entries.forEach { category ->
|
||||
assertFalse(
|
||||
"$category has nothing playable in a release build",
|
||||
GameCatalog.byCategory(category, includeComingSoon = false).isEmpty(),
|
||||
fun `hiding unfinished games still leaves something to play in every menu`() {
|
||||
// A menu filtered down to nothing would leave a stranded heading, and a
|
||||
// release where a whole section was empty would be a broken one.
|
||||
GameMenu.entries.forEach { menu ->
|
||||
assertTrue(
|
||||
"$menu has nothing playable in a release build",
|
||||
GameCatalog.hasAnything(menu, includeComingSoon = false),
|
||||
)
|
||||
}
|
||||
assertTrue(GameCatalog.playable.isNotEmpty())
|
||||
|
||||
Reference in New Issue
Block a user