"Cthulu Schmtulu" is a dice rolling game based on the board game Elder Sign from Fantasy Flight Games. The game revolves around rolling dice to match symbols on Adventure cards. The game is played by entering numbers or commands into the terminal. The goal is to collect the requisite number of Elder Signs before the chosen Great Old One is gains enough Doom to be summoned.
"Cthulu Schmtulu" is based on the board game Elder Sign from Fantasy Flight Games. The game revolves around rolling dice to match symbols on Adventure cards. The goal is to collect enough Elder Signs, through Rewards on Adventure cards, to banish the Great Old One before they have been summoned. The Great Old one accumulates Doom as the game progresses. When they have enough Doom, they will be summoned, eand the player loses. Players will also lose if they have 0 Sanity or Health at the end of a turn.
This is a terminal based text only implementation of a board game. It uses data directly from the board game to create various game objects. It is not a complete implementation in that many aspects of the game have not yet been implemented, which is something we would like to do in the future.
The expected user is anyone interested in playing a text based terminal game. It helps to understand how dice work. Being familiar with the Cthulu mythos is not necessary, but it will mean that the player is not as deterred by failure.
I used draw.io to make flow charts during the development of this project. They were helpful in the planning of the project, structuring of the code, and streamlining the main game play functions. As development progressed, my functions did begin to deviate from these flow charts.
Main game play function:
Apply outcome function:
I found the flow charts very helpful for conceptualizing the main game play loop.
Each game begins with a player entering their name, choosing a Great Old One to battle against, and choosing an Investigator to play as. After that the game begins. Each turn sees a player attempting to complete an Adventure. After the turn ends, in a success or failure, the player will be rewarded or penalized, their dice will be reset, and the clock will advance. If it is midnight, the Great Old One will accrue Doom (this happens every 3 turns). Then the game checks to see if any loss conditions or the win condition are satisfied. IF not, the game continues with the player going on another Adventure.
The only way to win is to collect the required amount of Elder Signs in order to banish the chosen Great Old One. This can only be done by completing Adventures that have Elder Signs as Rewards. A player will lose if their Health or Sanity is 0 at the end of a turn. A player will also lose if the Great Old One gains enough Doom to be summoned. Doom is gained every 3 turns (at the turns end), when the clock strikes midnight.
A player should decide in advance which Adventures they can afford to fail and which they can not. It is not wise to use all of your Items to complete an Adventure unless it will result in winning the game.
Note: The win and loss conditions are only checked at the end of the turn. If a player achieves both, then the win condition takes precedence.
A turn begins with a player going on an Adventure (drawing an Adventure "card") and rolling their dice. Each Adventure card has a name, flavor text, Tasks, Rewards, and Penalties. An Adventure is completed by completing each Task of the Adventure (in any order). If an Adventure is completed, then the player receives the listed award. If the player runs out of dice before completing the Adventure then they fail and suffer the Penalty. In either case, the turn ends, the game resets for the next round, and the next turn begins.
A Task on an Adventure consists of keywords (which represent symbols) and associated numbers. These keywords occur as faces on the dice. The player has, by default, 6 Green dice. A Green Die has the following 6 faces: 1 Investigate, 2 Investigate, 3 Investigate, 1 Lore, 1 Skulls, 1 Tentacles
A player will assign rolled dice from their dice pool to the Task in order to fulfill the requirements. For example, suppose the player rolled: Roll: 1 --> 2 Investigate; 2 --> 1 Skulls; 3 --> 1 Lore 4 --> 1 Lore; 5 --> 1 Tentacles; 6 --> 1 Investigate And has the Task: Remaining: 3 Investigate They would then enter 1 to assign their first Die to the Task. Their dice pool would then look like: Roll: 1 --> 1 Skulls; 2 --> 1 Lore 3 --> 1 Lore 4 --> 1 Tentacles; 5 --> 1 Investigate Remaining: 1 Investigate because they assigned there first Die to the Task. The requirement decreased by 2 since they had 2 Investigate "symbols" on that Die face. The Die was removed from their pool, and so all Die shifted down one. They might then enter 5 to assign their fifth Die to the Task. This would complete this Task and they would have 4 remaining dice to attempt the rest of the Tasks for the current Adventure.
Some Tasks require a player to lose health or sanity. These effects take place once the Task is chosen. The game only checks if the player is alive at the end of the turn, so a player can use an Item to recover even if their health or sanity reaches 0 during the turn.
Note: if a player rolls a wild face, then they choose which part of the Task to assign it to.
If none of their dice matched the requirements, the player can enter pass. This rerolls all of their dice at the cost of forfeiting one of them. Thus their dice pool decreases. If a player has only one remaining Die, and it does not match then they must pass. They will then fail the Adventure as they have no more dice. For example, a player may be in the following situation: Roll: 1 --> 2 Investigate; 2 --> 1 Skulls; 3 --> 1 Tentacles 4 --> 1 Investigate Remaining: 1 Lore They have no matching symbols and decide to pass. They might then see: Roll: 1 --> 1 Lore; 2 --> 3 Investigate; 3 --> 1 Investigate
Remaining: 1 Lore Now they can assign their first Die and complete the Task.
A player can also pass before choosing a Task to attempt. The effect is the same, and this may be necessary if they have no matching dice.
A player may use an Item before choosing a Task or assigning a Die. There are various Items with various effects. Entering Item will bring up the Item menu. It lists the name and effect of each Item. The different possible effects are:
Add a yellow Die, Add a red Die, Gain 1 Health, Gain 1 Sanity, Add a yellow and a red Die, Gain a wild Die, Reroll all dice, Restore Health and Sanity
A yellow Die has the following 6 faces: 1 Investigate, 2 Investigate, 3 Investigate, 4 Investigate, 1 Lore, 1 Skulls
A red Die has the following 6 faces: 1 Wild, 2 Investigate, 3 Investigate, 4 Investigate, 1 Lore, 1 Skulls
A wild Die has all 6 faces 1 Wild.
Each Investigator begins with different collection of starting Items that are drawn from the Item deck at the start of the game. After an Item is used, it is sent to the Item_discard. Players gain Items by completing Adventures. Clues and Spells are considered Items but are not in the Item_deck. They are unlimited in supply and are not sent to the Item_discard.
When an Adventure has all of its Tasks completed, the Adventure is complete and the player receives an Award. This can be various Items (drawn from the Item deck), more Health or Sanity, or an Elder Sign (maybe more than one). These are added to the players inventory. When a player fails an Adventure, by running out of dice, they will suffer the Penalty. This can be losing a Clue, the Great Old One gaining more doom, or losing Health or Sanity. These outcomes are automatically applied and then the turn ends.
Once a game has ended, a record is made of the result. This includes the name the player entered, the start time of the game, the end time of the game, and the result of the game, such as "Vincent Lee perished and Hastur devoured the world."
The main feature is the game play itself.
The game features an introduction screen with the option to get basic instructions for the game.
The player is able to enter their name, select the Great Old One, and select their Investigator. This is done through a menu that presents the relevant statistics for the individual choices. This data is recorded in a spreadsheet at the games conclusion
The turn begins with the player going on an Adventure. They are shown what the Tasks are for completing the Adventure, as well as what Rewards and Penalties are associated with the Adventure.
When the turn ends, the player is informed of the current game state.
When the game ends, the player is told of the results.
There are various moves the player can execute. There is the pass move which rerolls dice at the cost of losing a Die.
There is the possibility to use Items (players are brought to an Item selection screen).
The primary move is to assign dice to a Task.
The results of a completed game are recorded on this google spreadsheet on the "Records" page.
There are many directions that this application can go in the future:
- Different aspects of the original game: monsters, focusing, mythos cards, abilities, multiple players, etc.
- Save ability with distinct players.
- Help menu that can be called at various times.
- More game data, coming from expansions to the original game.
- Replacing the keywords on dice and Tasks with actual unicode symbols.
- Adding difficulty modification settings for further customization, like starting dice pool or how fast the clock is.
I manually tested the Python code through use of print statements. Other students, my mentor, my brother, and a friend also used tested the game.
As this project involved an interface that was provided via the Code Institute and we were not to edit it, I did not test the project in different browsers. I have been told that the template does work in Safari.
I used Flake8 to validate my code. I got the following types of notifications:
- trailing whitespace
- blank line contains whitespace
- missing space after ':', ',' and around operators
- import not used or variable not used
I fixed all of these issues. The remaining issues that Flake8 flags are contained in the files from Code Institute that are part of the template and I am explicitly not supposed to edit.
I encountered the following bugs:
-
There was an error loading the db_utilities file. The gspread package was throwing an exception.
Fix: I had replaced all ':' with ': ', and then ': ' with ': '. This made 'https://' into 'http: //' inside the SCOPE variable. I found this issue using DiffChecker. I removed the space.
-
Using a clue was throwing an error.
Fix: This was because the relevant function still had investigator.reroll() when I had moved the method to the Game object. I changed investigator.reroll() to game.reroll()
-
The fit_to_screen function was not working properly for flavor text.
Fix: This was because I was calling it once in the construction of the Adventure object and then again when I printed the flavor text. I now only call fit_to_screen when printing the flavor text.
-
The draw Item function was not drawing certain Item types.
Fix: This was due to some things being capitalized, and others not being capitalized. This the '==' checks were failing. I fixed this by calling lower() when doing the '==' check.
-
An exception was being thrown when attempting to parse the Die objects.
Fix: This was do to a refactor introducing a bank space at the end of some Die faces. I fixed this by removing the blank space.
-
The Penalty for removing clues was not working.
Fix: This was because the order of effect and Item_type were incorrect in the construction of clues (as well as spells.) I fixed this by correcting the order.
-
KeyError being thrown when assigning terms to the TRANSLATION dictionary object.
Fix: This was do to data in the spreadsheet not being normalized. There were typos as well as plural cases I was not accounting for. I fixed this by adding extra key, value pairs to the dictionary, adding redundancy.
-
Resetting Tasks was not working.
Fix: This was because when I defined remaining attr of the Task object it pointed to Task.pattern. This meant that when I modified the values of Task.remaining it modified those of the Task.pattern. I fixed this by copying the pattern dict using a dictionary comprehension.
-
I was getting a KeyError in the TRANSLATION dictionary for Elders.
Fix: It was showing up as a key because I was replacing " Sign" with "" instead of replacing " Signs". I fixed this by first replacing " Signs" with "" and then replacing " Sign" with "".
-
Some input completely passed through the attempt_Task function.
Fix: It was because the validation step at the very beginning of attempt_Task needed to be moved. This validation step has since been removed.
-
The translate_term function was being passed ' ' and that was throwing a key error.
Fix: I wasn't applying split to part inside the for loop iterating through parts. I applied the split method to part variable inside the relevant for loop.
-
Selecting an already complete Task rerolls dice for free.
Fix: I fixed this by changing where the reroll happens as well as checking if a Task is complete before allowing the player to attempt it.
-
'Terror)' was being passed as a key. This was a typo in the spreadsheet.
Fix: I edited the spreadsheet to remove the typo.
-
The whiskey Item had the wrong effect.
Fix: I edited the key for the associated function in ITEM_EFFECT dictionary.
-
The draw_Item method draws the same Item repeatedly.I had not yet started removing the Item I was drawing from the Item_deck.
Fix: I started removing the Items from the Item_deck inside the draw_Item method
-
Some statements were being reported too often. It looked like:
Only 7 more Doom is needed to summon Hastur to this plane of existence. Only 10 more Elder Signs is needed to banish Hastur and save the world. Oh no! Vincent Lee has been defeated. Now nothing stands in the way of Hastur. Only 7 more Doom is needed to summon Hastur to this plane of existence. Only 10 more Elder Signs is needed to banish Hastur and save the world. Only 7 more Doom is needed to summon Hastur to this plane of existence. Only 10 more Elder Signs is needed to banish Hastur and save the world. Only 7 more Doom is needed to summon Hastur to this plane of existence. Only 10 more Elder Signs is needed to banish Hastur and save the world.Fix: This was because I had print statements inside a property that was being called. It meant that every time I checked a condition the print statement was executing. I fixed this by removing print statements from these properties that are used for checking conditions.
-
Entering 'n' still resulted in the printing of the dice instructions.
Fix: This was because the condition was not correct. I fixed this by fixing the condition that was being checked.
-
An error was being thrown because I was trying to access game.health.
Fix: I fixed this to game.investigator.health
-
A typo was preventing the application from running.
Fix: There was an unterminated string that was causing this. I terminated the string.
- Go to Google Cloud Platform.
- Make sure you are logged into the Google account that you want to associate with this project (as opposed to a work account).
- Open side navigation bar by clicking on the "burger" icon in the upper left.
- Click on "select a project" and choose "New Project".
- Enter a project name and click "Create", and select this new project to go to the project page.
- Select "APIs & Services" from the menu on the left, and then select Library. We will be enabling the Google Drive API and the Google Sheets API.
If necessary, navigate back to the Dashboard for the current project, click on "APIs & Services" then Library in the menu on the left.
- Search for the Google Drive API in the search bar. Select it, and then enable it.
- Click "Create Credentials" in the upper right. Once at the form, select Google Drive API in the "Which API are you using?" dropdown menu. Select Application Data, then click "Next".
- Choose a name for the Service Account. Specify a Service Account ID if one is not generated from the name. Provide a description for the service, such as "Allow for communication between the app and Google Drive." Then click "Create and Continue".
- Select Role of Editor and click "Continue".
- On the "Grant players access to this service account" section, leave it blank and click "Done".
- Once back at the starting Google Drive API page, scroll down to service accounts and click on the account you have just created.
- Click on "Keys" in the menu at the top. Click on the "Add Key" dropdown menu and select "Create new Key". In the pop-up menu, select JSON and click "Create".
- Find the downloaded key in your on your machine (usually in your Downloads folder), its name will begin with the name of the service account it is associated with.
- Add it to your local repository, change its name (to creds for example), and then add it to your .gitignore file.
- Go to the credentials file in your local repository. Find the client email and copy it without the quotes.
- Go to the Google Sheets account and open the Sheet you want to grant access to and click the share button.
- Paste in the copied email address, make sure editor is selected, untick notify people, and click share.
If necessary, navigate back to the Dashboard for the current project, click on "APIs & Services" then Library in the menu on the left.
- Search for the Google Sheets API and select it.
- Click "Enable". Note: This API does not require credentials.
- Copy/Clone the repository on github.
- Log in to your Heroku account.
- From the Heroku Dashboard, click the dropdown menu "New" and select "Create new app".
- Choose a unique name for your app, shoose the appropriate region, and then click "Create app".
- Go to the "Settings" tab. Scroll to "Config Vars" section anc click "Reveal Config Vars".
- In field for key, enter "CREDS". In the field for value, paste the contents of your creds.json file which you created in the Google Drive API section above.
- Add a second Config Var with key "PORT" and value "8000".
- Scroll down to "Buildpacks". Click "Add buildpack", select "python", and click "Add buildpack".
- Click "Add buildpack", select "nodejs", and click "Add buildpack".
Note: Make sure that the python buildpack is before the nodejs buildpack. If not, you can reorder them by dragging python to the top.
- Go to the "Deploy" tab. Scroll down to "Deployment method" and select "GitHub". Search for your repository that you copied/cloned in step 1 above. Click "Connect" once you have found it.
- Scroll down to "Manual deploy" and click "Deploy Branch". Once the build is complete, click "View" to be taken to your deployed app.
The Love Sandwiches walkthrough project was very helpful. The explanation of how to deploy the project, and connect APIs was directly taken from that. I also used what I learned in that project to interact with google spreadsheets with gspread.
During the initial stages of development I looked at lists of single player games. I found Elder Sign on the following list.
I got great suggestions from my mentor about important aspects of the project, like implementing an analogue of a "GET" and "POST" method. He also supplied me with examples of readmes from previous projects.
Multiple other people played the game during development. My brother Anderson had helpful suggestions, as did my friend Mike. My fellow students Tarek, Alexander, Hollinda, and Anders had very helpful insights and were very supportive. They found bugs and tested the game.
The project was coded in python. I used:
- git and GitHub for version control,
- the python packages gspread, and google-auth-oauthli
- the Code Institute Template for PP3 for the interface of the deployed project,
- Heroku to host the project,
- Draw.io for flow charts.
- Flake8 for validation
- IPython for a console/repl
- SO: Long strings that conform to PEP8
- SO: implimenting getItem
- SO: Type hints for player defined classes
- SO: str and repr with Python lists
- SO: Type hinting with default parameters
- SO: default empty list parameter
- SO: storing functions as values in a dict
- SO: too many function arguments and PEP8 compliance
- SO: type hinting iterators
- Select random element from a tuple
- Spreadsheet of Elder Signs game data
- My version of the above spreadsheet is available in read only form here. In contains a subset of the data from the above. It has also been edited in very minor ways
- Elder Signs Official Rulebook
- Elder Sign play through from Geek and Sundry








