{"id":24443791,"url":"https://github.com/endlessm/clubhouse","last_synced_at":"2025-10-08T23:25:31.137Z","repository":{"id":44572123,"uuid":"146357685","full_name":"endlessm/clubhouse","owner":"endlessm","description":"Clubhouse for Endless Hack","archived":false,"fork":false,"pushed_at":"2023-01-12T03:49:18.000Z","size":146933,"stargazers_count":7,"open_issues_count":10,"forks_count":8,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-14T03:14:05.185Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/endlessm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-27T21:43:49.000Z","updated_at":"2023-11-24T12:44:12.000Z","dependencies_parsed_at":"2023-02-09T10:02:28.249Z","dependency_job_id":null,"html_url":"https://github.com/endlessm/clubhouse","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/endlessm/clubhouse","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endlessm%2Fclubhouse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endlessm%2Fclubhouse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endlessm%2Fclubhouse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endlessm%2Fclubhouse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/endlessm","download_url":"https://codeload.github.com/endlessm/clubhouse/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endlessm%2Fclubhouse/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000733,"owners_count":26082862,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-20T22:17:36.513Z","updated_at":"2025-10-08T23:25:31.102Z","avatar_url":"https://github.com/endlessm.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Clubhouse\n\nThe Clubhouse is one of the main applications in the Hack image, taking the\nuser on a guided journey through the world of computing.\n\n\n## Architecture\n\nThe Clubhouse is developed in Python as a GTK application.\nIt has a main CSD window and a shell component to provide custom system level\ndialogs.\n\n### GTK Application\n\nThe GTK application contains both the UI and also the Quests code, even\nthough the latter is decoupled from the main application code (as a\ndifferent module for now, but in the future it may become a different\nprocess even).\nThe main window adapts it size to the screen resolution using different CSS\nclasses 'small' for resolutions \u003c= 800p, 'big' for \u003e= 1080p and none for the rest\nThe main difference between those classes is the default font size 12px, 14px\nand 16px respectively the rest of the values like margins and padding should be \nin relative units like em and ex so that it adapts automatically.\n\n### Shell\n\nThe Clubhouse has a [Shell component](https://github.com/endlessm/gnome-shell/blob/master/js/ui/components/clubhouse.js) with the main functions:\n 1. Provide the Shell Quest View:\n   There are two quest views (the dialogs that represent a quest) associated\n   with the Clubhouse, one in the GTK application itself, and one in the\n   Shell so it's displayed on top of every area in the Shell, even when the\n   application's window is closed.\n\n## Development\n\nA few helper scripts can be found in the `tools` folder.\n\n### Building a Flatpak\n\nThe Clubhouse is distributed as a Flatpak, and it's advisable that it's\ndeveloped and debugged as a Flatpak too. To help building the Flatpak, we have\nthe `build-local-flatpak.sh` script.\nThis script changes the Flatpak's manifest module for the Clubhouse from a\n`git` type, to a `dir` type (meaning it will build files even when not\ncommitted to git yet, which is normally the case when developing), and creates\na local Flatpak repo for the app.\nThe script also takes any extra arguments for `flatpak-builder`, thus, if you\nwant to quickly build a Clubhouse Flatpak with any changes you may have done,\nand install it in the user installation base, you can do:\n\n`./tools/build-local-flatpak.sh --install`\n\n### Coding Style\n\nThe `run-lint` script can be used to verify the codebase's coding style. The\nlint check is run by the local build script mentioned above, so the build\nshould fail if there are lint issues. It also means that the lint is run on\nany PR on Github.\n\nThere's also a convenience script to set up a git commit hook that runs the\nmentioned lint script: `setup-git-hooks`. It will use Python's `virtual-env`\nto install the lint module locally with `pip` which also ensures that all\ndevelopers run the same version of the lint module.\n\n**Pro tip:** If you work frequently in the Clubhouse, it may be time consuming\nto have the lint check running on every build, thus, in order to avoid that, the\nlint check is actually turned off by the `build-local-flatpak.sh` script if the\ngit pre-commit hook is set up.\n\n### Building the documentation\n\nThe `build-docs` script can be used to build the documentation.\n\n## Quest Dialog Information\n\nFor easier authoring of the story, the quests use information that is edited\nseparately in a spreadsheet, one per row. The minimum information is, one per\ncolumn:\n\n- **Message ID**: The quest authors add it to the spreadsheet and use it in\n  quests to reference the text below and the rest of the information. The\n  message ID doesn't usually change.\n\n- **Text**: The text itself. Quest authors usually add temporary text. Then the\n  text is changed and fine-tuned at any time by the story writters. Sometimes\n  simple text markup can be used, see below.\n\nThis is enough for cases like button labels. Dialogue boxes have more\n(optional) information:\n\n- **Speaker**: The character saying the words. Defaults to the main character\n  of the quest.\n\n- **Animation**: The animation to play for the character speaking. Defaults to\n  a talking animation.\n\n- **SFX**: Code for a sound effect to replace the default dialogue popup\n  sound. It will only play if it's a valid code in the sound server.\n\n- **BG**: Code for a background sound. It lasts until the last consecutive\n  dialog box that has this same sound listed as BG is closed. It will only play\n  if it's a valid code in the sound server.\n\nMessage IDs should be prefixed with a quest name, or with the special prefix\n`NOQUEST`. Otherwise the information related to this ID won't be imported. For\nexample if a quest `LostFiles` exist, then good message IDs are:\n\n- `LOSTFILES_ABORT`\n- `LOSTFILES_QUESTION`\n- `LOSTFILES_HELLO`\n- `LOSTFILES_FLIP`\n- `LOSTFILES_FLIP_HINT1`\n- `LOSTFILES_THE_END`\n- `NOQUEST_ADA_NOTHING`\n\nIn a quest, message IDs can be used to display different kinds of\ndialogues. The prefix can be omitted, in which case the quest's name will be\nused as prefix. This means less writing and also less changes if e.g. we decide\nto change the quest's name for some reason:\n\n``` python\nself.wait_confirm('HELLO')\nself.show_hints_message('FLIP')\nself.show_message('THE_END', choices=[('End of Episode', self.finish_episode)])\n```\n\nThe `NOQUEST` prefix is used for all the message IDs that aren't intended for a\nspecific quest.\n\n#### Quest String Catalog\n\nThe general way to get the information related to a message ID is through the\n`QuestStringCatalog`class. This is what methods like `show_message()` use\ninternally. Usually you don't have to use the catalog directly, but if do:\n\n``` python\nfrom eosclubhouse.utils import QuestStringCatalog\n\n# Get some info:\ninfo = QuestStringCatalog.get_info('LOSTFILES_HELLO')\n\n# Get some text:\ntext = QuestStringCatalog.get_string('NOQUEST_ADA_NOTHING')\n```\n\n#### Text Markup\n\nSimple text markup can be used in the text for dialogs (not for text in labels\nor other places.)\n\nExample 1: ``We have *bold*, _italics_ and `code`.``\n\n![Example 1](data/docs/markup1.png?raw=true)\n\nExample 2: `*I am _very_ excited* about this quest!`\n\n![Example 2](data/docs/markup2.png?raw=true)\n\nExample 3: `I ~despise~ am not a fan of soup.`\n\n![Example 3](data/docs/markup3.png?raw=true)\n\nExample 4: ``Try setting `gravity = 0` in the code.``\n\n![Example 4](data/docs/markup4.png?raw=true)\n\n### Special Quest Message IDs\n\nThere are message IDs treated specially. For example, to override default\nmessages. Below, `MYQUEST` should be read as the prefix of a valid quest:\n\n#### Messages For Accepting And Aborting Quests\n\n- `MYQUEST_ABORT`: Quests are usually aborted when the external app they are\n  using is closed by the user. The information for this ID will be used to\n  display a message when that happens. If the information has a SFX, it will\n  override the default sound played when quests are aborted. A BG sound will be\n  ignored.\n\n- `MYQUEST_QUESTION`: Overrides the default message that appears when a quest\n  is proposed (e.g. when a character is clicked and has a quest for the user to\n  do). If the information has a SFX, it will override the default sound played\n  when quests are proposed. A BG sound will be ignored.\n\n- `MYQUEST_QUEST_ACCEPT`: Overrides the default label used to accept the\n  proposed quest. Note this is a label, so only the text information is used.\n\n- `MYQUEST_QUEST_REJECT`: Overrides the default label used to reject the\n  proposed quest. Note this is a label, so only the text information is used.\n\n#### Hints Messages\n\nYou can use the same message ID with suffixes `_HINT1`, `_HINT2`, etc to\ndisplay a message with a number of hints. The message will loop between initial\ntext and all the hints in sequence. For example if you have message IDs in the\nspreadsheet like:\n\n- `MYQUEST_FLIP`\n- `MYQUEST_FLIP_HINT1`\n- `MYQUEST_FLIP_HINT2`\n- `MYQUEST_FLIP_HINT3`\n\nYou can display the message with hints in the quest with:\n\n``` python\nself.show_hints_message('FLIP')\n```\n\n#### Hint Message Used To Launch App\n\n- `MYQUEST_LAUNCH`, `MYQUEST_LAUNCH_HINT1`, ...: If these message IDs are\n  defined for the quest, a hint message will be displayed automatically when\n  the quest asks the player to launch an app.\n\nThe API to ask the player to launch an app is:\n\n``` python\nself.ask_for_app_launch(self._app, pause_after_launch=2)\n```\n\n#### Characters Idle Messages\n\nWhen a character has no quest to propose to the player, it will display an idle\nmessage. In this message the character will try to point the player to a\ncharacter that does have something to propose. And if none of the characters\nhave anything to propose, it will fallback to a \"NOTHING\" message. The special\nmessage IDs to build the idle messages are as follows. Considering two\ncharacters, Ada and Saniel:\n\n- `NOQUEST_ADA_SANIEL`: The text Ada displays to point the player to Saniel,\n  when Ada has nothing to offer.\n\n- `NOQUEST_SANIEL_ADA`: The text Saniel displays to point the player to Ada,\n  when Saniel has nothing to offer.\n\n- `NOQUEST_ADA_NOTHING`: The text Ada displays when none of the characters have\n  a quest to offer.\n\n- `NOQUEST_SANIEL_NOTHING`: The text Saniel displays when none of the\n  characters have a quest to offer.\n\nThe above messages will be used in any episode, independently from whether they\nhave been set in a sheet corresponding to a certain episode in the spreadsheet.\n\nIt is also possible to specify `NOQUEST` messages that episode dependent. This\nis done by using `NOQUEST_EPISODEID` instead of just NOQUEST, where `EPISODEID`\nis the ID of the episode, i.e. for `episode3` specific `NOQUEST` messages, they\nshould have the prefix of `NOQUEST_EPISODE3`. The suffixes work just as\nmentioned above for the plain `NOQUEST` messages, e.g.\n`NOQUEST_EPISODE2_SANIEL_ADA` will be used when the user clicks on the Saniel\ncharacter in episode 2, when this character has no quest to offer but the Ada\ncharacter does.\n\n`NOQUEST` messages specific to episodes should be kept in the episode's\nrespective sheet.\n\n**Note**: This is the default behavior. It can be changed for a\ncharacter by overriding the `QuestSet.get_empty_message()` method.\n\n### Importing Quest Dialog Information From The Spreadsheet\n\nTo automatically fetch the latest changes in the spreadsheet,\nthere is the `get-strings-file` script. Usually you want to commit the changes\nas well:\n\n    ./tools/get-strings-file --commit\n\nThere are more options to fetch specific pages or rows. Call\n`./tools/get-strings-file --help` for details.\n\nInternally, the information is stored in CSV files, and exposed through the\n`QuestStringCatalog` class.\n\n### Quests/Strings Alternative Location\n\nSometimes it is convenient to be able to run quests or change dialog strings\nwithout having to build a new Flatpak. Thus, any quests' code, or a modified\nstrings CSV file can be added to a secondary location and will be loaded\ndirectly by the Clubhouse (overriding any quest/string-id with the same name).\nthis alternative location is: `~/.var/app/com.hack_computer.Clubhouse/data/quests`\n\nTo automatically fetch the spreadsheet into the alternative path:\n\n    ./tools/get-strings-file --use-alternative-path\n\n### ActivityCard Alternate Background Images\n\nWhen adding new new quest it is covenient to have an alternative directory\nto put cards background images in for easy prototyping.\n\nThis is why the app will load any jpg image named as the quest id from\n`~/.var/app/com.hack_computer.Clubhouse/data/quests/cards/` directory\n\nSo if you are adding an quest with an id 'my-new-quest', all you have to do is\nadd an my-new-quest.jpg file in that directory\n\n### Importing Other Information From The Spreadsheet\n\nBesides the quest strings, there is more information in other pages of the\nspreadsheet. Currently: episode names and badges, inventory item names and\ndescriptions.\n\nUse the `get-info-file` script to import this information, by passing the\nspreadsheet page as argument:\n\n`./tools/get-info-file episodes`\n\n### Building the Quests Flow\n\nWhen you are creating an episode, you start with a diagram like this:\n\n![Example 1](data/docs/quests-flow.png?raw=true)\n\nTo build the graph of this diagram, you should:\n\n- Define quest-sets. Each quest-set has an ordered list of quests. Each\n  quest-set is mapped to a character in the Clubhouse, and represents the set of\n  quests offered by this character.\n\n- Define the availability dependency between quests of different quest-sets. The\n  dependency between quests of same quest-sets is automatic: they are made\n  available in order, as defined in the previous step.\n\n- Define which quests are automatically offered, if any.\n\n- Define which quest marks the episode as completed.\n\n- Define which quest advance automatically to the next episode. Should be the\n  last quest of an episode.\n\nYou define the quests in each quest-set like this:\n\n``` python\nclass QuestSetA(QuestSet):\n\n    __quests__ = ['QuestA1', 'QuestA2', 'QuestA3']\n```\n\n**Note**: The order in the names A1, A2, A3 above are just an example. There is\nno convention for the class name of quests.\n\nTo define the availability dependency between quests of different quest-sets:\n\n``` python\nclass QuestA3(Quest):\n\n    __available_after_completing_quests__ = ['QuestB1']\n```\n\nIn the diagram above, quests A2 and A3 are auto-offered once they become\navailable. The character will offer the next quest immediately after the\nprevious quest is completed. So the player will get the impression that quests\nA1 to A3 are chained together. To make a quest auto-offer:\n\n``` python\nclass QuestA2(Quest):\n\n    def setup(self):\n        self.auto_offer = True\n```\n\nOne of the quests must mark the episode as completed. This doesn't have to be\nthe last one. Quests define if they complete the episode with:\n\n``` python\nclass QuestC2(Quest):\n\n    __complete_episode__ = True\n\n```\n\nThe quests coming after the episode is completed are considered bonus. In the\nexample above, quests B3 and B4 are bonus.\n\nIf there are more episodes after this one, the last quest should advance to the\nnext episode with:\n\n``` python\nclass QuestB4(Quest):\n\n    __advance_episode__ = True\n\n```\n\n### Sprite Animations Format\n\nCharacter animations in the Clubhouse are implemented with\nsprites. Each sprite is composed of two files:\n\n- A PNG file that contains all the animation frames, in sequence.\n\n- A JSON file with the metadata required to load and play the frames\n  in the PNG as an animation.\n\nThis is the format of the JSON file:\n\n```json\n{\n  \"default-delay\": 100,\n  \"frames\": [\n    \"0 750\",\n    1,\n    2,\n    3,\n    \"4 2000-3000\",\n    3,\n    2,\n    1,\n    \"0 2250-6250\"\n  ],\n  \"height\": 306,\n  \"width\": 147\n}\n```\n\nAll frames are the same size, and is defined by \"width\" and \"height\"\nin the metadata.\n\nThe sequence of frames and timings are defined by \"frames\" in the\nmetadata. The frame numbers are zero-based. The sequence has the frame\nnumber, optionally accompanied by a delay. If a delay is not provided,\n\"default-delay\" in the metadata will be used. The delay can be a\nnumber in milliseconds, or it can be a range like `2000-3000`\nabove. If a range is provided, a random delay will be picked from the\nrange each time the animation is played. Thus, the same frame can be\nreused with different timing in the sequence. The example above\ndefines an animation using 5 frames. It will:\n\n- Display frame 0 for 750 milliseconds.\n- Display frames 1 to 3 in sequence, for the default delay of 100 milliseconds.\n- Display frame 4 for a random delay between 2 and 3 seconds.\n- Display frames 3 to 1 in reverse sequence, with default delay as before.\n- Display frame 0 for a random delay between 2250 and 6250 milliseconds.\n\nThe Clubhouse plays the animations in loop.\n\n### Character Animations Alternative Location\n\nThere is an alternative path where a designer/developer can place a\ncharacter animation that is used instead of the default one. Animators\ncan use this to test new character animations and edit the current\nones without re-installing the clubhouse. This alternative location\nis: `~/.var/app/com.hack_computer.Clubhouse/data/characters`\n\nExample:\n\n```\nmkdir -p ~/.var/app/com.hack_computer.Clubhouse/data/characters/ada/fullbody\n(git clone https://github.com/endlessm/clubhouse)\n(cd clubhouse)\ncp data/characters/ada/fullbody/hi.* ~/.var/app/com.hack_computer.Clubhouse/data/characters/ada/fullbody\n\n# Change the animation format here and make Ada go crazy. Example: \"frames\": [0,8,0,8]\ngedit ~/.var/app/com.hack_computer.Clubhouse/data/characters/ada/fullbody/hi.json\n\n# Restart the clubhouse:\ncom.hack_computer.Clubhouse -x \u0026\u0026 com.hack_computer.Clubhouse -d\n```\n\n## Reloading the Clubhouse\n\nThe Clubhouse may still be running even when its window is closed, thus, for\nforcing it to quit (in order to e.g. re-run it after adding new content to the\nalternative quests' location), you can simply press Ctrl+Escape in its window\n(you may have to close and re-open it for the focus to be properly set and the\nkeyboard shortcut to work).\n\n## Running Modes For Development\n\n### Debug Mode\n\nThe Clubohouse has a debug mode for developers. It will add debug\nlines to the logs.\n\nTo set debug mode in the Clubhouse, call:\n\n``` bash\ncom.hack_computer.Clubhouse --debug\n```\n\nLogs are directed to the main instance of the Clubhouse. So if the\nClubhouse was running when you called `--debug`, you won't see any\nlogs in the Terminal, and the command will exit immediately. If you\nwant to see debug logs in the Terminal, you will have to first quit\nand then start with debug mode again, like this:\n\n``` bash\ncom.hack_computer.Clubhouse --quit\ncom.hack_computer.Clubhouse --debug\n```\n\n### Measuring Performance\n\nOutput the time it takes to run certain setup steps. Useful for finding\nperformance regressions:\n\n``` bash\nCLUBHOUSE_PERF_DEBUG=yes com.hack_computer.Clubhouse\n```\n\n### Profiling\n\nOutput a file with statistics of the current profile.\n\n``` bash\nCLUBHOUSE_PROFILING=yes com.hack_computer.Clubhouse\n```\n\nA new file named `clubhouse-runstats` will be created.\n\nYou can visualize it with the Python tool `snakeviz` by running\n\n```\npip3 install --user snakeviz\nsnakeviz clubhouse-runstats\n```\n\nTo create a Graphviz diagram `dot` file, you can do:\n\n```\npip3 install --user gprof2dot\ngprof2dot -f pstats clubhouse-runstats \u003e profile.dot\n```\n\nand to visualize it you can run\n\n```\ndot -Tsvg -o profile.svg \u003c profile.dot\n```\n\nIf you do not have the `dot` binary, do not worry. You can also visualize\nit in on-line in sites like [dreampuf](https://dreampuf.github.io) or\n[GraphvizFiddle](https://stamm-wilbrandt.de/GraphvizFiddle/).\n\n## Translations\n\nTo add a new language for the clubhouse interface you should add it to the\npo/LINGUAS file.\n\nYou can start a new translation just copying the po/clubhouse.pot file and\nstart translating to the desired language.\n\nTo update existing language translations:\n\n```\nninja -C build/ clubhouse-update-po\n```\n\n## Future Work ##\n\n### Separating the Quests from the Clubhouse ###\n\nThe quests consist of Python code that is run in-process with the\nClubhouse.\nThe quest code is made simpler than the Clubhouse code due to the use of\nlibquest to abstract away some of the details, but there is still room\nto make it even simpler.\n\nWriting quests with Python code still means the quest writer has to be\nat least familiar with this language and paradigm. Moreover, for\nstability reasons we still follow a review process for the quests code,\nwhich can extend the time required for a quest to be fully integrated.\nThis solution limits who we can hire to write the quests and\nmakes the time for writing them slower than it could be.\nThus, ours plans include changing the quests system so they can be\nwritten by a domain-specific language or a different paradigm like\nflow-based programming; this would allow us to even move the quests\nout of the Clubhouse so they're completely managed by a third party,\nand it lowers the requirements for the quest writer's profile/knowledge.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fendlessm%2Fclubhouse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fendlessm%2Fclubhouse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fendlessm%2Fclubhouse/lists"}