Skip to content

WIP: Achievements#118

Open
bb-froggy wants to merge 72 commits into
Sesu8642:masterfrom
bb-froggy:dev-achievements
Open

WIP: Achievements#118
bb-froggy wants to merge 72 commits into
Sesu8642:masterfrom
bb-froggy:dev-achievements

Conversation

@bb-froggy

@bb-froggy bb-froggy commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

This PR is a Work in Progress for #101 Achievements.

I currently don't believe I require immediate feedback (although I am thankful for it), but here is where I track this task.

Status

So far, it implements the basic data and class structure for achievements. It also includes a UI page, which needs some attention. A couple of achievements are already added.

I rebased on February 19 after #116 was merged, therefore the initial commits all have that date.

Open Tasks

General/Architecture

  • Define a class for historic emperors and events. Achievements may, but don't need to, have such a historic connection. If they do, it defines their name and the history description will be pre-pended to the achievement's description. Usually, the "last achievement in a row" will get the historic connection.
  • Shall we align with Show some statistics when a game is over #95 somehow? They have some tasks in common. One of them is that if a player does something like destroy a capital and then presses undo, it should probably not be counted for neither the achievements nor for the per-game statistics.
  • Secret Achievements

UI

  • The background color for each achievement is very similar between finished and in-progress achievements. The black font for the "Unlocked" is hard to read. This is because the Window color somehow tints everything and so far I haven't been able to define the font and background color in absolute terms.
  • The two-line headings don't fit well in the achievement boxes (the single-line ones neither, but they are okay). Enlarging the achievement boxes would solve this problem only partially, as they are currently Window widgets and I had difficulties controlling the title in a way that would solve this. Possibly, a different kind of widget would be better. Also, you can currently move the achievement boxes around because they are Windows.
  • Achievement boxes shall get a black outline.

Achievements

We had lots of them defined and we may add others later. We need some "definition of done" for this PR and then we can add other achievements later on. There are many good ideas, but some of them will be hard to implement at least right now and it would take a lot of work to implement really all of them.

Achievements for which the current GameExitedEvent suffices

  • WinNGamesAchievement with n = 1, 10, 50
  • WinVeryHardGamesInARowAchievement with n = 3, 10, 20
  • WinOnMapSizeAchievement with every map size
  • WinAgainstAiLevelAchievement for every AI level
  • Win in at most x turns against AI level very hard on map size Medium or larger with x = 18, 14, 12
  • Win when starting last against AI level "Very Strong"
  • Lose against weakest AI
  • Win (enemies give up) and there are still kingdoms of three (more?) colors left
  • Play a game for over X turns with X=30, 50
  • Abort a game

Achievements using castle counting

- [x] WinWithOnlyNCastlesAchievement with n = 0, 1, 3
- [x] BuyNCastlesAchievement with = 1, 20, 100

Unit Tests

  • Add Unit tests for the achievements to see whether they unlock when expected

@Sesu8642

Copy link
Copy Markdown
Owner

I had a brief look at your approach: scanning all moves "live" as they happen. It has the advantage that the player could be alerted about any new achievements right away. Disadvantages include:

  • the need to handle undoing moves
  • potentially running checks over and over again
  • limited knowledge about the previous turns and moves

Another approach could be to record all player moves during the game. It shouldn't be too difficult as incremental player moves are already recorded for the undo feature. You could take the same data (+ end of turn markers) and record the whole game. The recording could be added to the GameExitedEvent and then checked for any unlocked achivements.

The same data could also be used for other features, like offering to save the replay of a game for later re-watching or sharing. But that's certainly out of scope for this PR 😄

@bb-froggy

Copy link
Copy Markdown
Contributor Author

The undo was already giving me some headaches. I tried to solved it for the "buy n castles" case, but for other ideas, it would be much easier if we had a fixed linear history and just scanned through that to forward the progress of the achievements. Your have convincing arguments. I think that this suggestion is a great idea.

Thus, I would scope this PR to just include achievements that work with only the current GameExitedEvent, which is a nice collection already IMHO. Everything else, I would remove again from the PR and we save that for a later modification.

Or: We leave the existing castle-counting code of AchievementGameStateTracker in there, but not add anything else that uses this approach, as it allows for the (quite challenging) low-castle achievements and the "buy n castle" achievements -- but once we have that later modification with the recorded game we will refactor the code to use that and remove the AchievementGameStateTracker. The one disadvantage I see is that we have the AchievementGameStateTracker listening for three events and writing to preferences for each of them, which might have a performance impact.

@bb-froggy

bb-froggy commented Mar 3, 2026

Copy link
Copy Markdown
Contributor Author

I noticed that I also subscribed to the RegenerateMapEvent in WinVeryHardGamesInARowAchievement. I guess that's also okay, it won't happen mid-game, not so often in total, and there's no "Undo". I need it to prevent people from skipping maps that look difficult to avoid breaking their streak.

@bb-froggy bb-froggy marked this pull request as draft May 13, 2026 20:36
@bb-froggy

bb-froggy commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

Known open issues:

  • Using the localization
  • German translations
  • Achievement UI: unlocked and locked achievement boxes should have the same size
  • Test that achievement state is persisted correctly
  • When unlocking an achievement, the SlideStage UI seems not to update
  • @Sesu8642 's feedback on service and events architecture and injecting the achievements

@Sesu8642

Copy link
Copy Markdown
Owner

I consider the localization nice to have for this, as it was added after this PR was already far along and you probably want to get it done soon.

@Sesu8642 Sesu8642 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gave it another review.

magFilter: Nearest
characters: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()[]{}%&$§!?=+-.,:;_/\\*~#\"'↗–äöüÄÖÜßẞ\n"
}
MenuHeading: {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this font used somewhere? If not, it can be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used by menu_heading, defined in SkinConstants as FONT_MENU_HEADING, and used as the heading in the dialog that opens when you tap on a specific achievement. The important thing is that it is white. This is better readable in the dialog box with its turquois/cyan/green-blue (?) background.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is generated and should not be edited directly. Instead, Skin Composer should be used. In addition to that, a new label style should be created and scaled in SkinFactory. Using the tool can be a bit tricky. Can I help you with that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean "do everything for me", then you have my permission ;-). If you have some pointers, and I'll take it from there, I would also be glad. I guess "Skin Composer" is that tool (=a standalone program?).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take care of it :)

* Provides achievement classes, knows how to construct each individual achievement
*/
@Singleton
public class AchievementProvider {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably call this AchievementFacory, as provider may be confused with the @provides concept of Dagger.

Speaking of which, there could be a provideAchievments method in a dagger module that uses this factory and makes the list of achievements injectable. That way, the AchievementService can be simplified as it only calls this method once anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds interesting. I wasn't aware of this dagger concept.

this.prefStore = achievementsPrefs;
}

public void LoadPersistedAchievements(Iterable<AbstractAchievement> achievements) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like that side effect on the achievements. Maybe the achievementProvider/achievement list can be injected into the repository directly instead of the service. Then this method could use that instead of the parameter and just return the populated list to the service.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the code and now there is an AchievementsFactory with a Factory Method. I first wanted to use the dagger provides I had just learned about, but ultimately didn't use it.

My issue was that the AchievementsFactory "provides" a list of raw achievement classes. The AchievementRepository provides a list of achievement classes with the values loaded from the prefStore. I can only dagger-provide one of them (or use different collection classes to differentiate ... but that would be more of a hack). It felt unclean to me to provide the raw ones, because everywhere in the code except for the AchievementRepository itself, we'll want to have the ones with the loaded values. Thus, AchievementRepository uses the AchievementsFactory explicitly and calls the Factory Method. And AchievementService needs the reference to AchievementRepository anyway, so it feels redundant to have it injected both the AchievementsRepository AND the list of achievements. Hence, it also explicitly retrieves the achievements from the AchievementsRepository where it needs them and doesn't store a separate list of achievements. Consequently, the AchievementsSlide now gets its achievements for displaying also from the AchievementsRepository now and not the AchievementService.

@bb-froggy

bb-froggy commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

I have addressed your concerns, @Sesu8642 . Please have a look, especially at the ones where I haven't yet resolved the conversation -- I didn't follow your suggestions by the letter in these cases and therefore I am not sure your concern is fully resolved.

Everything is hard-coded English as of yet.

The AI levels are shown as LEVEL1, LEVEL2, ... because the method converting them to readable display names wasn't there anymore -- I guess it needs a different approach now respecting the translations. This isn't even what it should be for English.

@bb-froggy bb-froggy marked this pull request as ready for review May 20, 2026 20:55
@Sesu8642

Copy link
Copy Markdown
Owner

Sorry for the delay. I'm focusing on release 1.5 right now, which I want to get out soon. I'll review your latest changes after that.

@Sesu8642

Sesu8642 commented Jun 5, 2026

Copy link
Copy Markdown
Owner

It looks like there are some comments of mine from Mar 22 which you might have missed. Can you please have another look?

RICHARD_THE_LIONHEART("Richard the Lionheart", "Richard I. (1157-1199) was King of England. He fought many battles, especially in the Third Crusade, far away from his home."),
FREDERICK_THE_GREAT("Frederick the Great", "Frederick II. (1712-1786) was King of Prussia. While winning many battles, he is also known as a patron for education and the arts."),
LOUIS_XI("Louis XI", "Louis XI (1423-1483) was King of France and had the nickname 'The Universal Spider', as he carried out intrigues to play out his enemies against each other. He is known for his cunning and deviousness, but also for uniting France after the Hundred Years' War."),
HENRY_VIII("Henry VIII", "Henry VIII (1491-1547) was King of England. He is known for his six marriages, but also for building much military infrastructure."), // Reserved for BuyNCastlesAchievement

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the Henry ones are unused.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw your comments not. I guess they can stay until the castle achievement is implemented.

@Sesu8642

Sesu8642 commented Jun 6, 2026

Copy link
Copy Markdown
Owner

I started moving strings over to the translation files. Some auto-formatting happened. Please make sure to pull my changes to avoid conflicts.
I'm aware many texts are not displayed properly yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants