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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ WORKDIR /app
# front-end (examples/web_app.py) could not give them.
COPY requirements.txt ./
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& pip install --no-cache-dir "tak @ git+https://github.com/Stephenson-Software/tak@v0.2.0" \
&& pip install --no-cache-dir "tak @ git+https://github.com/Stephenson-Software/tak@963cd3c94c07a98eaeff31f6a9796b90bd023e64" \
&& apt-get purge -y git && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*

COPY src/ ./src/
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
# tak is the text-adventure kit extracted from this game
# (https://github.com/Stephenson-Software/tak), which FishE now runs on.
# Installing it needs git on the PATH.
tak @ git+https://github.com/Stephenson-Software/tak@eedf89fe142221c430787837d43d29d3ccfa7f07
tak @ git+https://github.com/Stephenson-Software/tak@963cd3c94c07a98eaeff31f6a9796b90bd023e64
jsonschema>=4.0,<5.0
68 changes: 68 additions & 0 deletions src/achievements/achievements.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,22 @@
GOAL_AMOUNT = 10000
GOAL_MILESTONE_NAME = "Reached Goal"

# Achievements on arcade (https://danielstephenson.dev/play, Stephenson-Software
# RFC 0014): a curated subset of the milestones below carries an "arcadeId",
# plus the goal and the retirement that ends a run. The same ids are declared
# for the game "fishe" in the gateway's config/play/boards.yaml, and are
# permanent: never rename one. "arcadeHidden" marks one the arcade page lists
# as "Hidden achievement" until it is earned. See achievements/arcade.py.
GOAL_ARCADE_ID = "reached-goal"
GOAL_DESCRIPTION = "Reach $10,000"
RETIRED_ARCADE_ID = "retired"
RETIRED_TITLE = "Retired"
RETIRED_DESCRIPTION = "Retire with your fortune made"

MILESTONES = [
{
"name": "First Catch",
"arcadeId": "first-catch",
"stat": "totalFishCaught",
"threshold": 1,
"description": "Catch your first fish",
Expand All @@ -28,6 +41,7 @@
},
{
"name": "Master Fisher",
"arcadeId": "master-fisher",
"stat": "totalFishCaught",
"threshold": 1000,
"description": "Catch 1,000 fish",
Expand All @@ -40,6 +54,7 @@
},
{
"name": "Big Earner",
"arcadeId": "big-earner",
"stat": "totalMoneyMade",
"threshold": 1000,
"description": "Earn $1,000 total",
Expand All @@ -52,6 +67,7 @@
},
{
"name": "First Mate",
"arcadeId": "first-mate",
"stat": "totalWorkersHired",
"threshold": 1,
"description": "Hire your first crew member",
Expand All @@ -70,6 +86,7 @@
},
{
"name": "Took the Helm",
"arcadeId": "took-the-helm",
"stat": "totalVoyagesCaptained",
"threshold": 1,
"description": "Captain a voyage yourself",
Expand All @@ -82,6 +99,8 @@
},
{
"name": "Letter of Marque",
"arcadeId": "letter-of-marque",
"arcadeHidden": True,
"stat": "totalRaids",
"threshold": 1,
"description": "Put a boat to piracy for a day",
Expand All @@ -100,6 +119,7 @@
},
{
"name": "First Export",
"arcadeId": "first-export",
"stat": "totalFishExported",
"threshold": 1,
"description": "Ship your first fish to another village",
Expand All @@ -118,12 +138,14 @@
},
{
"name": "Homeowner",
"arcadeId": "homeowner",
"stat": "highestHomeTier",
"threshold": 2,
"description": "Buy your first home",
},
{
"name": "Waterfront Manor",
"arcadeId": "waterfront-manor",
"stat": "highestHomeTier",
"threshold": 5,
"description": "Own the finest home in the village",
Expand All @@ -136,6 +158,7 @@
},
{
"name": "Property Mogul",
"arcadeId": "property-mogul",
"stat": "totalPropertiesBought",
"threshold": 5,
"description": "Buy 5 investment properties over your career",
Expand Down Expand Up @@ -168,3 +191,48 @@ def getNewlyEarned(stats):
stats.earnedMilestones.append(milestone["name"])
newly.append(milestone)
return newly


def arcadeIdForName(name):
"""The arcade achievement id an earned milestone name stands for, or None.

Covers the goal too, which is recorded on stats.earnedMilestones under
GOAL_MILESTONE_NAME like any milestone."""
if name == GOAL_MILESTONE_NAME:
return GOAL_ARCADE_ID
for milestone in MILESTONES:
if milestone["name"] == name:
return milestone.get("arcadeId")
return None


def arcadeDeclarations():
"""Every arcade achievement the game can unlock, as the gateway's
boards.yaml declares them: [{id, title, description, hidden}], in order."""
declared = [
{
"id": milestone["arcadeId"],
"title": milestone["name"],
"description": milestone["description"],
"hidden": bool(milestone.get("arcadeHidden", False)),
}
for milestone in MILESTONES
if "arcadeId" in milestone
]
declared.append(
{
"id": GOAL_ARCADE_ID,
"title": GOAL_MILESTONE_NAME,
"description": GOAL_DESCRIPTION,
"hidden": False,
}
)
declared.append(
{
"id": RETIRED_ARCADE_ID,
"title": RETIRED_TITLE,
"description": RETIRED_DESCRIPTION,
"hidden": False,
}
)
return declared
54 changes: 54 additions & 0 deletions src/achievements/arcade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# @author Daniel McCoy Stephenson
"""FishE's achievements on arcade (Stephenson-Software RFC 0014), via tak.arcade.

Each function here is called at the moment something is earned - a milestone
announced, the goal reached, the run retired - and on load, to re-assert what
an existing save has already earned. tak.arcade.unlock is fire-and-forget and
idempotent: it does nothing outside the browser on arcade or for a player who
is not signed in, and the service ignores a repeat. Nothing here reads or
writes save state beyond reading stats.earnedMilestones; a failure is
swallowed, never raised into the game.
"""

from achievements.achievements import (
RETIRED_ARCADE_ID,
arcadeIdForName,
)


def unlock(achievementId):
"""Report one achievement. Never raises."""
try:
from tak import arcade

arcade.unlock(achievementId)
except Exception:
pass


def milestonesEarned(milestones):
"""Report the arcade ids of milestones just earned (rows of MILESTONES)."""
for milestone in milestones:
achievementId = milestone.get("arcadeId")
if achievementId:
unlock(achievementId)


def nameEarned(name):
"""Report the arcade id an earned milestone name (or the goal) stands for."""
achievementId = arcadeIdForName(name)
if achievementId:
unlock(achievementId)


def retired():
unlock(RETIRED_ARCADE_ID)


def catchUp(stats):
"""Re-assert every achievement a loaded save has already earned.

For a save that earned milestones before arcade achievements existed, or
while the player was signed out. Reads stats.earnedMilestones only."""
for name in list(getattr(stats, "earnedMilestones", []) or []):
nameEarned(name)
7 changes: 7 additions & 0 deletions src/fishE.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from tak.saves import chooseSlot, deleteSlot
from browserSaveSync import syncBrowserSaves
from achievements import achievements
from achievements import arcade as arcadeAchievements
from achievements.achievements import GOAL_AMOUNT, GOAL_MILESTONE_NAME
from progression import progression
from config.config import Config
Expand Down Expand Up @@ -175,6 +176,10 @@ def __init__(self, interfaceType=INTERFACE_TYPE):
# player (see progression.catchUp).
progression.catchUp(self.player, self.stats)

# Re-assert on arcade the achievements this save has already earned
# (fire-and-forget; reads stats.earnedMilestones, writes nothing).
arcadeAchievements.catchUp(self.stats)

# The game opens on the docks, with fishing as the only thing on the
# menu. Everywhere else in the village is revealed as it is earned -
# see src/progression.
Expand Down Expand Up @@ -259,6 +264,7 @@ def _runGameLoop(self):
newlyEarned = achievements.getNewlyEarned(self.stats)
for milestone in newlyEarned:
self.prompt.text += " [Milestone unlocked: %s!]" % milestone["name"]
arcadeAchievements.milestonesEarned(newlyEarned)

# announce the one thing the player has just opened up, with the
# reason they opened it, so the newly-appeared menu entry is
Expand Down Expand Up @@ -301,6 +307,7 @@ def announceGoalIfReached(self):
" [GOAL REACHED! You've built your fortune of $%d! "
"Keep fishing, or retire from the Home menu.]" % GOAL_AMOUNT
)
arcadeAchievements.nameEarned(GOAL_MILESTONE_NAME)
return True
return False

Expand Down
3 changes: 3 additions & 0 deletions src/location/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from stats.stats import Stats
from ui.userInterface import UserInterface
from achievements import achievements
from achievements import arcade as arcadeAchievements
from housing import housing
from progression import progression

Expand Down Expand Up @@ -295,4 +296,6 @@ def retire(self):
"Here's how your career adds up:",
"",
] + self._statsLines()
# The run's ending, reported to arcade as it happens.
arcadeAchievements.retired()
self.userInterface.showDialogue("\n".join(lines))
Loading
Loading