Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 29 additions & 163 deletions apps/adventure-arena/catalog/Adventures.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-- Editorial library order; new Arena Originals follow the established selection.
-- Artwork paths are local; descriptions and reviews preserve the source text.
-- Artwork paths are local. Ratings stay empty until sourced reader feedback exists.
-- The former static review copy had no reader provenance; see git c055005.
-- `collection` groups the library into shelves, `difficulty` uses Infocom's
-- published levels, `tint` is the game's jewel-tone accent taken from its cover
-- (it must carry white text at 4.5:1), `tintDark` is the same hue lightened to
Expand All @@ -20,23 +21,10 @@ return {
description = "Ensign Seventh Class of the Stellar Patrol — that's you. Your ship, the S.P.S. Feinstein, has been destroyed. You escaped in a life pod and crash-landed on a strange, seemingly abandoned planet. Explore the ruins of a lost civilization, befriend an unforgettable robot named Floyd, and unravel the mystery of what happened here. A landmark in interactive storytelling with genuine emotional depth.",
author = "Infocom",
year = 1983,
rating = 4.7,
reviewCount = 2104,
rating = 0,
reviewCount = 0,
startFile = "planetfall.zil",
reviews = {
{
author = "floyd_fan",
rating = 5,
title = "Floyd is unforgettable",
body = "Planetfall is charming, funny, and surprisingly moving. Floyd the robot is one of the greatest NPCs in gaming history.",
},
{
author = "scifi_explorer",
rating = 5,
title = "Meretzky's masterpiece",
body = "Steve Meretzky's debut game is a triumph. The writing is witty, the world is richly described, and the puzzles are fair.",
},
},
reviews = {},
genre = "Sci-Fi Adventure",
cover = "apps/adventure-arena/assets/planetfall.jpg",
},
Expand All @@ -52,24 +40,11 @@ return {
description = "Something is very wrong. You wake up in a place that shouldn't exist, surrounded by people who seem to know you — but you don't remember them. Navigate through fractured realities, solve deeply unsettling puzzles, and piece together the truth about who you really are.\n\nSanitarium is a slow-burn psychological horror experience where the most terrifying thing isn't what lurks in the shadows — it's what you might discover about yourself.",
author = "Studio Null",
year = 2024,
rating = 4.5,
reviewCount = 183,
rating = 0,
reviewCount = 0,
startFile = "blackwood-horror.zil",
base = "infocom/zork1",
reviews = {
{
author = "horrorFan99",
rating = 5,
title = "Genuinely unsettling",
body = "I had to stop playing at night. The atmosphere is so thick you can feel it. One of the best horror text adventures I've played.",
},
{
author = "interactive_fiction_nerd",
rating = 4,
title = "Slow start, incredible payoff",
body = "Give it an hour before judging. The first chapter is setting up something deeply twisted, and when it all clicks, it's unforgettable.",
},
},
reviews = {},
genre = "Psychological Horror",
cover = "apps/adventure-arena/assets/sanitarium.jpg",
},
Expand All @@ -85,23 +60,10 @@ return {
description = "Ten years after the events of Enchanter, the very foundations of magic itself seem to be failing. As leader of the Circle of Enchanters, you must uncover the cause of this paralyzing chaos. Journey through the black foundation of magic itself in a perilous quest to save your world. Spellbreaker is the riveting conclusion to the Enchanter trilogy, and Infocom's most challenging game.",
author = "Infocom",
year = 1985,
rating = 4.4,
reviewCount = 891,
rating = 0,
reviewCount = 0,
startFile = "z6.zil",
reviews = {
{
author = "mage_master",
rating = 5,
title = "Brutally difficult, deeply rewarding",
body = "This is Infocom at its hardest. Every puzzle requires careful thought and creative spellcasting. Not for the faint of heart.",
},
{
author = "enchanter_trilogy",
rating = 4,
title = "A satisfying finale",
body = "The Enchanter trilogy ends on a high note. The spell system is ingenious and the puzzles are well-crafted, if extremely tough.",
},
},
reviews = {},
genre = "Fantasy Adventure",
cover = "apps/adventure-arena/assets/spellbreaker.jpg",
},
Expand All @@ -117,30 +79,11 @@ return {
description = "You are Pip, a wind-up apprentice no taller than a teacup, working for Grandfather Tolliver, the last toymaker in Wrenfold. Every night after the shop closes, the toys wake up, and Pip keeps the workshop running until morning.\n\nTonight, Tolliver is missing and the workshop key has stopped ticking on its hook. Without it, the toys will fall silent by sunrise. Find out what happened, recover the key, and rewind the town's heart before dawn.",
author = "Studio Null",
year = 2024,
rating = 4.7,
reviewCount = 286,
rating = 0,
reviewCount = 0,
startFile = "wondertown.zil",
base = "infocom/zork1",
reviews = {
{
author = "ClockworkReader",
rating = 5,
title = "Whimsical with real heart",
body = "The toy workshop is charming from the first room, but the story has more emotional weight than I expected. Pip is a wonderful protagonist.",
},
{
author = "ParserFan",
rating = 5,
title = "A warm modern Infocom homage",
body = "The WIND and OIL puzzles are clever, fair, and perfectly matched to the setting. It feels nostalgic without being stuck in the past.",
},
{
author = "ToyboxTactician",
rating = 4,
title = "Cozy, clever, and bittersweet",
body = "The puzzles are approachable, but the ending choices still made me pause. A lovely all-ages adventure.",
},
},
reviews = {},
genre = "Whimsical Adventure",
cover = "apps/adventure-arena/assets/wondertown.jpg",
},
Expand All @@ -156,30 +99,11 @@ return {
description = "The year is 1888. Lord Ashworth, a wealthy industrialist, has been found dead in his locked study at Ashworth Manor in the Limehouse district. Scotland Yard is baffled. Lady Ashworth has hired you to investigate before the press gets wind of the scandal.\n\nAs a private detective in Victorian London, you must explore the manor, gather evidence, and interrogate three suspects: the nervous butler Mr. Hudson, the cold Lady Ashworth, and the arrogant Dr. Moriarty. Five key pieces of evidence point to the killer, but can you connect the clues before they escape?",
author = "Studio Null",
year = 2024,
rating = 4.6,
reviewCount = 342,
rating = 0,
reviewCount = 0,
startFile = "limehouse-killings.zil",
base = "infocom/zork1",
reviews = {
{
author = "IF_Weekly",
rating = 5,
title = "A masterclass in interactive mystery",
body = "The Limehouse Killings is a triumph of interactive fiction. The atmospheric writing transports you to Victorian London, the puzzles are challenging but fair, and the story keeps you guessing until the very end.",
},
{
author = "TextAdventureFan",
rating = 5,
title = "Victorian London has never felt so real",
body = "The writing in this game is exceptional. You can smell the coal smoke and river damp, feel the fog on your face. The manor house is full of character, and the NPCs feel like real people.",
},
{
author = "PuzzleMaster",
rating = 4,
title = "Challenging but fair",
body = "I got stuck on the cipher puzzle for an hour, but when I figured it out, I felt like a genius. The game gives you all the clues you need — you just have to find them.",
},
},
reviews = {},
genre = "Victorian Mystery",
cover = "apps/adventure-arena/assets/limehouse.jpg",
},
Expand All @@ -195,23 +119,10 @@ return {
description = "A blizzard rages outside G.U.E. Tech. You brave the storm to reach the computer lab and finish your term paper, but your file has been corrupted — partially overwritten by the Department of Alchemy's files. As you investigate, you descend into steam tunnels and ancient chambers where something ancient and malevolent stirs. Infocom's only horror game, inspired by the works of H.P. Lovecraft.",
author = "Infocom",
year = 1987,
rating = 4.3,
reviewCount = 723,
rating = 0,
reviewCount = 0,
startFile = "h1.zil",
reviews = {
{
author = "lovecraft_reader",
rating = 5,
title = "Lovecraftian perfection",
body = "Dave Lebling captures the cosmic horror atmosphere perfectly. The descriptions are chilling, and the sense of dread is palpable.",
},
{
author = "crawler_tunnels",
rating = 4,
title = "Atmospheric and tense",
body = "The best-written Infocom game I've played. The blizzard setting, the creepy campus, the ancient evil — it all works beautifully.",
},
},
reviews = {},
genre = "Psychological Horror",
cover = "apps/adventure-arena/assets/lurkinghorror.jpg",
},
Expand All @@ -227,29 +138,10 @@ return {
description = "You are standing in an open field west of a white house, with a boarded front door. You are about to begin a legendary adventure that started it all — a journey into the Great Underground Empire. Navigate treacherous mazes, collect treasures, and outwit the fearsome Grues that lurk in the dark.\n\nOriginally created at MIT in the late 1970s, Zork pioneered the text adventure genre and established many of the conventions still used in interactive fiction today.",
author = "Infocom",
year = 1980,
rating = 4.8,
reviewCount = 2341,
rating = 0,
reviewCount = 0,
startFile = "zork1.zil",
reviews = {
{
author = "dungeon_crawler",
rating = 5,
title = "A timeless classic",
body = "Still holds up decades later. The writing is sharp, the puzzles are fair, and exploring the dungeon never gets old. This is where it all began.",
},
{
author = "text_adventurer",
rating = 5,
title = "The gold standard",
body = "If you've never played a text adventure, start here. Zork set the bar so high that most games since have been playing catch-up.",
},
{
author = "pixel_nostalgia",
rating = 4,
title = "Great but brutal",
body = "Zork rewards patience and lateral thinking. A few puzzles are obtuse by modern standards, but the atmosphere and writing more than make up for it.",
},
},
reviews = {},
genre = "Classic Adventure",
cover = "apps/adventure-arena/assets/zork1.jpg",
},
Expand All @@ -265,23 +157,10 @@ return {
description = "The Wizard appears, floating nonchalantly in the air beside you. He grins sideways at you. As you explore deeper into the Great Underground Empire, you'll face the Wizard's unpredictable magic, navigate treacherous new mazes, and uncover secrets even darker than those of Zork I. The sequel that expanded the legend.",
author = "Infocom",
year = 1981,
rating = 4.6,
reviewCount = 1872,
rating = 0,
reviewCount = 0,
startFile = "zork2.zil",
reviews = {
{
author = "wizard_watcher",
rating = 5,
title = "A worthy successor",
body = "The Wizard of Frobozz is one of gaming's great characters. Unpredictable, hilarious, and occasionally helpful — he makes this game unforgettable.",
},
{
author = "dungeon_diver",
rating = 4,
title = "Tougher than Zork I",
body = "The puzzles here are harder and more abstract. The Carousel Room alone is a rite of passage for any text adventure fan.",
},
},
reviews = {},
genre = "Classic Adventure",
cover = "apps/adventure-arena/assets/zork2.jpg",
},
Expand All @@ -297,23 +176,10 @@ return {
description = "You have reached the final test. The deepest and most mysterious reaches of the Great Underground Empire await. Nothing is as it seems, and the one responsible for the shadow and darkness — the Dungeon Master — embodies the greatest mystery of all. In this test of wisdom and courage, you will face countless dangers. But what awaits you at the culmination of your odyssey is well worth risking all.",
author = "Infocom",
year = 1982,
rating = 4.5,
reviewCount = 1567,
rating = 0,
reviewCount = 0,
startFile = "zork3.zil",
reviews = {
{
author = "trilogy_complete",
rating = 5,
title = "A profound conclusion",
body = "Zork III is different — more atmospheric, more philosophical. It rewards exploration and patience over brute-force puzzle solving.",
},
{
author = "retro_gamer",
rating = 4,
title = "A fitting end",
body = "Not as iconic as Zork I, but the somber, mysterious tone sets it apart. The ending is genuinely satisfying.",
},
},
reviews = {},
genre = "Classic Adventure",
cover = "apps/adventure-arena/assets/zork3.jpg",
},
Expand Down
4 changes: 4 additions & 0 deletions apps/adventure-arena/views/components/FeaturedCard.etlua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
<Label text="Read" systemImage="book.fill" size="15" weight="semibold" color="white" />
</HStack>
</GlassEffect>
<% if game.reviewCount > 0 then %>
<Label text="<%= string.format('%.1f', game.rating) %>" systemImage="star.fill" iconSize="12" spacing="4" size="13" weight="semibold" color="white" accessibilityLabel="Rated <%= string.format('%.1f', game.rating) %> out of 5" />
<% else %>
<Label text="No ratings yet" size="13" color="white" />
<% end %>
<Label text="<%= game.difficulty or game.genre %>" size="13" weight="medium" color="white" />
</HStack>
</VStack>
Expand Down
4 changes: 4 additions & 0 deletions apps/adventure-arena/views/components/GameRow.etlua
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
<VStack spacing="3" alignment="leading" flexGrow="1">
<Label text="<%= game.title %>" size="16" weight="semibold" lines="2" />
<Label text="<%= game.genre %> · <%= game.year %>" size="13" color="secondary" lines="1" truncation="tail" />
<% if game.reviewCount > 0 then %>
<HStack spacing="4">
<%- partial("Stars.etlua", { rating = game.rating, size = 10 }) %>
<Label text="<%= string.format('%.1f', game.rating) %>" size="12" color="secondary" />
</HStack>
<% else %>
<Label text="No ratings yet" size="12" color="secondary" />
<% end %>
</VStack>
</HStack>
</Button>
Expand Down
2 changes: 1 addition & 1 deletion apps/adventure-arena/views/pages/Detail.etlua
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<ScrollView id="facts" horizontal="true" vertical="false">
<HStack spacing="0" paddingHorizontal="8" paddingVertical="14" alignment="center">
<%- partial("../components/Fact.etlua", { caption = "Rating", value = string.format("%.1f", game.rating), detail = game.reviewCount .. " ratings", divider = true }) %>
<%- partial("../components/Fact.etlua", { caption = "Rating", value = game.reviewCount > 0 and string.format("%.1f", game.rating) or "—", detail = game.reviewCount > 0 and (game.reviewCount .. " ratings") or "No ratings yet", divider = true }) %>
<%- partial("../components/Fact.etlua", { caption = "Difficulty", value = game.difficulty or "—", detail = "Infocom level", divider = true }) %>
<%- partial("../components/Fact.etlua", { caption = "Genre", value = game.genre:gsub(" Adventure$", ""), detail = "Adventure", divider = true }) %>
<%- partial("../components/Fact.etlua", { caption = "Released", value = tostring(game.year), detail = game.author, divider = false }) %>
Expand Down
7 changes: 1 addition & 6 deletions tests/adventure_arena_model.test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,12 @@ t.expect(catalog:find("missing") == nil, "unknown game ids return nil")
t.assertEqual(catalog:all()[1].id, "infocom.planetfall", "catalog is sorted by title")
t.assertEqual(catalog:featured()[2].id, "books.blackwood-horror", "second featured game matches source")
t.assertEqual(catalog:featured()[3].id, "infocom.spellbreaker", "third featured game matches source")
local expectedCounts = { ["infocom.spellbreaker"] = 891, ["infocom.zork2"] = 1872,
["infocom.zork3"] = 1567, ["books.limehouse-killings"] = 342 }
for id, count in pairs(expectedCounts) do
t.assertEqual(catalog:find(id).reviewCount, count, "source review count for " .. id)
end
local reviews = 0
for _, game in ipairs(catalog:all()) do
reviews = reviews + #game.reviews
t.expect(#game.description > 200, "complete source description for " .. game.id)
end
t.assertEqual(reviews, 21, "all individual source reviews are preserved")
t.assertEqual(reviews, 0, "bundled catalog contains no unsourced reader feedback")
t.assertEqual(catalog:find("books.limehouse-killings").genre, "Victorian Mystery")
for _, game in ipairs(catalog:all()) do
t.expect(type(game.collection) == "string", "every game belongs to a library shelf: " .. game.id)
Expand Down
41 changes: 41 additions & 0 deletions tests/adventure_arena_ratings.test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
_G.__headless = true
local t = require("TestKit")
local xml = require("ui.xml")
local catalog = require("apps.adventure-arena.catalog.Adventures")

local originals = 0
for _, book in ipairs(catalog) do
if book.collection == "Arena Originals" then originals = originals + 1 end
t.assertEqual(book.rating, 0, "no unsourced aggregate: " .. book.id)
t.assertEqual(book.reviewCount, 0, "no unsourced reader count: " .. book.id)
t.assertEqual(next(book.reviews), nil, "no unsourced review text: " .. book.id)
t.expect(book.reviewCount >= #book.reviews, "count covers displayed reviews: " .. book.id)
end
t.assertEqual(originals, 6, "all six Arena Originals are audited")

local function render(path, game)
return xml.describeFile("apps/adventure-arena/views/" .. path .. ".etlua",
{ game = game, index = 1, id = "book", action = "open", related = {} }).source
end
for _, path in ipairs({ "components/FeaturedCard", "components/GameRow", "pages/Detail" }) do
for _, book in ipairs(catalog) do
local source = render(path, book)
t.expect(source:find("No ratings yet", 1, true), path .. " explains unreviewed state: " .. book.id)
t.expect(not source:find('name="star', 1, true) and not source:find('systemImage="star', 1, true),
path .. " does not present zero as a star score")
t.expect(not source:find("Rated 0.0", 1, true) and not source:find('id="review_', 1, true),
path .. " has no zero-rating announcement or review cards")
end
-- Synthetic feedback is test-only. Keep the genuine-feedback rendering path.
local rated = {}
for key, value in pairs(catalog[1]) do rated[key] = value end
rated.rating, rated.reviewCount = 4.5, 1
rated.reviews = {{ author = "Test reader", rating = 4, title = "Test title", body = "Test feedback" }}
local source = render(path, rated)
t.expect(not source:find("No ratings yet", 1, true), path .. " retains rated state")
t.expect(source:find("4.5", 1, true), path .. " retains supplied aggregate")
if path == "pages/Detail" then
t.expect(source:find("Test feedback", 1, true), "supplied reader text remains visible")
end
end
os.exit(t.summary() and 0 or 1)