diff --git a/luminous-lightyears/.github/workflows/lint.yaml b/luminous-lightyears/.github/workflows/lint.yaml
new file mode 100644
index 0000000..7f67e80
--- /dev/null
+++ b/luminous-lightyears/.github/workflows/lint.yaml
@@ -0,0 +1,35 @@
+# GitHub Action workflow enforcing our code style.
+
+name: Lint
+
+# Trigger the workflow on both push (to the main repository, on the main branch)
+# and pull requests (against the main repository, but from any repo, from any branch).
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+# Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit.
+# It is useful for pull requests coming from the main repository since both triggers will match.
+concurrency: lint-${{ github.sha }}
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ env:
+ # The Python version your project uses. Feel free to change this if required.
+ PYTHON_VERSION: "3.12"
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Run pre-commit hooks
+ uses: pre-commit/action@v3.0.1
diff --git a/luminous-lightyears/.gitignore b/luminous-lightyears/.gitignore
new file mode 100644
index 0000000..233eb87
--- /dev/null
+++ b/luminous-lightyears/.gitignore
@@ -0,0 +1,31 @@
+# Files generated by the interpreter
+__pycache__/
+*.py[cod]
+
+# Environment specific
+.venv
+venv
+.env
+env
+
+# Unittest reports
+.coverage*
+
+# Logs
+*.log
+
+# PyEnv version selector
+.python-version
+
+# Built objects
+*.so
+dist/
+build/
+
+# IDEs
+# PyCharm
+.idea/
+# VSCode
+.vscode/
+# MacOS
+.DS_Store
diff --git a/luminous-lightyears/.pre-commit-config.yaml b/luminous-lightyears/.pre-commit-config.yaml
new file mode 100644
index 0000000..4bccb6f
--- /dev/null
+++ b/luminous-lightyears/.pre-commit-config.yaml
@@ -0,0 +1,18 @@
+# Pre-commit configuration.
+# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.5.0
+ hooks:
+ - id: ruff
+ - id: ruff-format
diff --git a/luminous-lightyears/LICENSE.txt b/luminous-lightyears/LICENSE.txt
new file mode 100644
index 0000000..5a04926
--- /dev/null
+++ b/luminous-lightyears/LICENSE.txt
@@ -0,0 +1,7 @@
+Copyright 2021 Python Discord
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/luminous-lightyears/README.md b/luminous-lightyears/README.md
new file mode 100644
index 0000000..913178d
--- /dev/null
+++ b/luminous-lightyears/README.md
@@ -0,0 +1,262 @@
+
+
+
+
+Defcord
+
+Lead Nations. Compete. Outsmart.
+
+
+ Table Of Contents
+
+ -
+ About
+
+ -
+ Vital Attributes
+
+ -
+ Game Model
+
+ -
+ Available Commands
+
+ -
+ How the bot relate to the theme `Information Overload`
+
+ -
+ Known Issues
+
+ -
+ Enhancements
+
+ -
+ Contributions
+
+ -
+ Running the App
+
+ -
+ Demo of the game
+
+ -
+ A Video of the Gameplay
+
+
+
+
+## About
+
+Defcord is a multiplayer, discord application based game.
+
+You play as a leader of a nation and it is your responsibility to make good decisions based on the information given to you.
+
+You should make sure all of your vital attributes stay positive.
+
+You can't know what vital attributes increase/decrease for the given information for the decision you make because you know, people are complex.
+
+Whoever stays alive till the end of the game is the survivor of the game. If multiple people survive they are also survivors.
+
+The main goal is to make sure you won't run out of your vital attributes in order to survive.
+
+## Vital Attributes
+
+| Attribute | Default Value |
+|---------------|---------------|
+| Money | 100 |
+| Loyalty | 50 |
+| Security | 50 |
+| World Opinion | 50 |
+
+## Game Model
+
+One player is mapped with only one game. So at a time, a player can game participate in only one game.
+
+The game time will be a random time between `12.5` and `16` minutes.
+
+We have `3` stages, each stage will make the information flow faster.
+
+If the player is AFK or non-responsive to the information for `60` seconds, they're disqualified automatically.
+
+## Available Commands
+
+
+### `/defcord create`
+
+We use this command to create a defcord game. Whoever creates will be part of the game by default and they'll be the only one who can start the game.
+
+User needs to enter the number of players they want in the game and the nation name they want to be the leader of.
+
+Once the game is created, a message will be posted with the invite code. Other players need to use the invite code to join the game.
+
+A player can join from a different channel or even a different server. Only requirement is in that server `defcord` should be installed.
+
+
+### `/defcord join`
+
+Other payers need to use this command with the given/taken invite code from the game creator in order to join a `defcord` game.
+
+They also need to enter their nation name, via a modal prompt.
+
+
+### `/defcord start`
+
+Once the required number of players join, the game creator can start the game. Once invoked, the command will automatically start the game that the current player created and part of.
+
+After that, stage `1` begins. Players will receive information at a slow rate first, as the game progresses it'll get faster.
+
+Now the players should start respond to the information by making a good decision in order to survive.
+
+
+### `/defcord leave`
+
+If the player wants to leave in the middle of the game or before the game start, they can do. But if they leave in the middle of the game, they cannot rejoin. They can join another game.
+
+If you are the last member to quit, you are the survivor of the game as you have no one to compete. But you can play till the game time and see what the game brings you. If you run out of an attribute than you are done.
+
+
+## Theme Relativity
+
+Given theme is `Information Overload`. Here we try to make the player overwhelmed by giving them information continuously.
+
+They need to keep making good decisions in order to survive. Because they can't give `yes` to all the requests i.e. army tool requests, it'll cost their money. This is also applicable to the reverse situation where the player denies all requests.
+
+After every stage we show the player's attributes so they need to make a quick play to proceed with the next stage. We have AFK mechanism in place so they can't sit idle, information flow till the game end.
+
+## Known Issues
+
+- `/defcord start` will show as `Application did not respond` in the invalid use case scenarios instead of showing a relevant message to the user. This is due to a fact that we missed to `await` that message call during a code refactor. Invalid use case scenarios,
+ * Trying to start the game without being in one
+ * When in a game, trying to start it (only creator can)
+ * Trying to start a game is already running
+ * Trying to start the game before all the players join
+- We have an image embedded in each message to represent the actor of the message. But sometimes the service that we used to host the images goes down. So sometimes the images won't appear. If you receive any error due to this in the console or bot crashes due to this, you can set this env variable `WITHOUT_ACTOR_THUMBNAIL` to `True` to disable thumbnail functionality.
+
+## Enhancements
+
+- Include attribute sabotage and request mechanism so that it'll be more PvP instead of PvE.
+- Game time can be configurable by the creator.
+- Include encryption like mechanism to make it harder for the user.
+- Option to stop the game by the creator (current work around is to make all players leave / die).
+- Enhance random information picking logic to make it more relevant to the players context and reduce repetitiveness of information.
+- Option to pause the game.
+- Option to start the game if everyone has not joined.
+
+## Contributions
+
+- **Clueless_conoisseur** (krishnabhat): Checking PRs, structuring the project
+- **Automafun** (Dhanvantg): Basic game character formation, logo creation
+- **Diverman** (hazyfossa): Coding game factory, player classes, implementing character templates, weighted randomness logic
+- **Maheshkumar**: Coding button interactions, defcord start command, advanced UI components
+- **Sapient**: Basic UI elements, PlayerState class default values, Game class creation, game flow, Anti-AFK mechanism, character images
+
+## Running The App
+
+We require you to have `python 3.12`.
+
+### Clone
+
+```sh
+git clone https://github.com/krishnabhat3383/code-jam-24-luminous-lightyears.git
+```
+
+if you prefer ssh way,
+
+```sh
+git clone git@github.com:krishnabhat3383/code-jam-24-luminous-lightyears.git
+```
+
+### Move To Directory
+
+```sh
+cd code-jam-24-luminous-lightyears
+```
+
+### Create Virtual Environment
+
+```sh
+python3 -m venv .venv
+```
+
+### Activate Virtual Environment
+
+```sh
+source .venv/bin/activate
+```
+
+### Install Requirements
+
+```sh
+pip install -r requirements.txt
+```
+
+### Setup Bot Token
+
+```sh
+export DEFCON_BOT_TOKEN=
+```
+
+for a persistent way,
+
+```sh
+touch .env
+```
+
+```sh
+echo "DEFCON_BOT_TOKEN=" >> .env
+```
+
+### Run The Application
+
+```sh
+python main.py
+```
+
+## Demo
+
+After the starting of the bot, following would need to be done to start the game.
+
+1) To initiate the game the player (further referred as 'creator' of the game) need to use `/defcord create` and add the max number of players in the game.
+
+
+
+2) The creator will receive a modal, which will ask for their nation name.
+
+
+
+3) After entering the nation name the creator will receive 3 messages.
+
+ 1st message referring to them as a player and their nation name.
+
+ 2nd message is a game code, for anyone joining the game (visible to everyone in chat)
+
+ 3rd message is the standard joining message, indicating how many players are left to join and who has last joined (here the game created if of 2 players)
+
+
+
+
+
+4) Other wannabe players would need to use `/defcord join` with the invite code to join the game
+
+
+
+5) After everyone joining the game, everyone in the game will receive this message
+
+
+
+ (Here `Thonk` being the last player joined)
+
+6) After this the creator is able to use the command `/defcord start` to start the game, and then everyone will receive 3 messages (3rd one being part of the main game, hence covered below)
+
+
+
+7) Game flow - example
+
+ ![flow](https://github.com/user-attachments/assets/df7e944a-a361-47c5-862a-34a4b44f0bf0)
+
+
+## Video Showcase
+https://youtube.com/shorts/Aox39yoCAXY
+
+
+[Move To Top](#defcord)
diff --git a/luminous-lightyears/main.py b/luminous-lightyears/main.py
new file mode 100644
index 0000000..57d4d14
--- /dev/null
+++ b/luminous-lightyears/main.py
@@ -0,0 +1,60 @@
+import logging
+from os import getenv
+from sys import argv, exit
+
+from interactions import (
+ Client,
+ Intents,
+ listen,
+)
+
+logger = logging.getLogger("defcon-internal")
+logging.basicConfig(level=logging.INFO)
+
+bot = Client(intents=Intents.DEFAULT, logging_level=logging.INFO)
+
+
+@listen()
+async def on_ready() -> None:
+ """Notify that the bot is started."""
+ logger.info("Bot started.")
+
+
+def get_token() -> str:
+ """Try to read bot's token from environment or .env."""
+ try:
+ from dotenv import load_dotenv
+
+ load_dotenv()
+ no_dotenv = False
+ except ImportError:
+ no_dotenv = True
+
+ token = getenv("DEFCON_BOT_TOKEN")
+
+ if token is None:
+ logger.error("Token not found.\nPlease specify discord bot token via environment variable 'DEFCON_BOT_TOKEN'.")
+
+ if no_dotenv:
+ logger.info("To read token from .env, install 'python-dotenv'")
+
+ exit()
+ return token
+
+
+def get_developer_mode() -> bool:
+ """Get if the bot is running in dev mode."""
+ try:
+ return argv[1] == "--dev"
+ except IndexError:
+ return False
+
+
+DEV = get_developer_mode()
+
+if __name__ == "__main__":
+ if DEV:
+ bot.load_extension("interactions.ext.jurigged")
+
+ bot.load_extension("src.game_interaction")
+ bot.start(token=get_token())
diff --git a/luminous-lightyears/pyproject.toml b/luminous-lightyears/pyproject.toml
new file mode 100644
index 0000000..6d45047
--- /dev/null
+++ b/luminous-lightyears/pyproject.toml
@@ -0,0 +1,31 @@
+[tool.ruff]
+# Increase the line length. This breaks PEP8 but it is way easier to work with.
+# The original reason for this limit was a standard vim terminal is only 79 characters,
+# but this doesn't really apply anymore.
+line-length = 119
+# Target Python 3.12. If you decide to use a different version of Python
+# you will need to update this value.
+target-version = "py312"
+# Automatically fix auto-fixable issues.
+fix = true
+# The directory containing the source code. If you choose a different project layout
+# you will need to update this value.
+src = ["src"]
+
+[tool.ruff.lint]
+# Enable all linting rules.
+select = ["ALL"]
+# Ignore some of the most obnoxious linting errors.
+ignore = [
+ # `id` as arg
+ "A002",
+ # Init doc strings
+ "D107",
+ # We are using f-strings in logger
+ "G004",
+ # We never use random for cryptographic purposes
+ "S311",
+]
+
+[tool.ruff.lint.per-file-ignores]
+"src/characters/**.py" = ["PLR2004"]
diff --git a/luminous-lightyears/requirements-dev.txt b/luminous-lightyears/requirements-dev.txt
new file mode 100644
index 0000000..d529f2e
--- /dev/null
+++ b/luminous-lightyears/requirements-dev.txt
@@ -0,0 +1,6 @@
+# This file contains all the development requirements for our linting toolchain.
+# Don't forget to pin your dependencies!
+# This list will have to be migrated if you wish to use another dependency manager.
+
+ruff~=0.5.0
+pre-commit~=3.7.1
diff --git a/luminous-lightyears/requirements.in b/luminous-lightyears/requirements.in
new file mode 100644
index 0000000..dc1e440
--- /dev/null
+++ b/luminous-lightyears/requirements.in
@@ -0,0 +1,42 @@
+# This file was autogenerated by uv via the following command:
+# uv pip compile requirements.txt
+aiohttp==3.9.5
+ # via discord-py-interactions
+aiosignal==1.3.1
+ # via aiohttp
+attrs==23.2.0
+ # via
+ # aiohttp
+ # discord-py-interactions
+croniter==2.0.7
+ # via discord-py-interactions
+discord-py-interactions==5.13.1
+ # via -r requirements.txt
+discord-typings==0.9.0
+ # via discord-py-interactions
+emoji==2.12.1
+ # via discord-py-interactions
+frozenlist==1.4.1
+ # via
+ # aiohttp
+ # aiosignal
+idna==3.7
+ # via yarl
+multidict==6.0.5
+ # via
+ # aiohttp
+ # yarl
+python-dateutil==2.9.0.post0
+ # via croniter
+pytz==2024.1
+ # via croniter
+six==1.16.0
+ # via python-dateutil
+tomli==2.0.1
+ # via discord-py-interactions
+typing-extensions==4.12.2
+ # via
+ # discord-typings
+ # emoji
+yarl==1.9.4
+ # via aiohttp
diff --git a/luminous-lightyears/requirements.txt b/luminous-lightyears/requirements.txt
new file mode 100644
index 0000000..f8da89a
--- /dev/null
+++ b/luminous-lightyears/requirements.txt
@@ -0,0 +1,2 @@
+discord-py-interactions~=5.13.0
+python-dotenv~=1.0.1
diff --git a/luminous-lightyears/samples/Pipfile b/luminous-lightyears/samples/Pipfile
new file mode 100644
index 0000000..27673c0
--- /dev/null
+++ b/luminous-lightyears/samples/Pipfile
@@ -0,0 +1,15 @@
+# Sample Pipfile.
+
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+
+[dev-packages]
+ruff = "~=0.5.0"
+pre-commit = "~=3.7.1"
+
+[requires]
+python_version = "3.12"
diff --git a/luminous-lightyears/samples/pyproject.toml b/luminous-lightyears/samples/pyproject.toml
new file mode 100644
index 0000000..835045d
--- /dev/null
+++ b/luminous-lightyears/samples/pyproject.toml
@@ -0,0 +1,19 @@
+# Sample poetry configuration.
+
+[tool.poetry]
+name = "Name"
+version = "0.1.0"
+description = "Description"
+authors = ["Author 1 "]
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = "3.12.*"
+
+[tool.poetry.dev-dependencies]
+ruff = "~0.5.0"
+pre-commit = "~3.7.1"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/luminous-lightyears/src/__init__.py b/luminous-lightyears/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/luminous-lightyears/src/characters/__init__.py b/luminous-lightyears/src/characters/__init__.py
new file mode 100644
index 0000000..b5aba77
--- /dev/null
+++ b/luminous-lightyears/src/characters/__init__.py
@@ -0,0 +1,28 @@
+"""Import and store all the characters and templates for use."""
+
+from importlib import import_module
+from logging import getLogger
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from src.weighted_random import WeightedList
+
+if TYPE_CHECKING:
+ from src.templating import Actor
+
+log = getLogger("character-init")
+
+all_characters: WeightedList["Actor"] = WeightedList()
+
+for file in Path(__file__).parent.glob("*.py"):
+ module = file.name.split(".")[0]
+
+ if not file.is_file() or not module.endswith("_chr"):
+ continue
+
+ try:
+ all_characters.append(character := import_module(f"src.characters.{module}").character)
+ log.debug("Loaded %s character from %s.", character.name, file)
+ except Exception:
+ log.exception("Character file %s is invalid. Skipping.", file)
+ continue
diff --git a/luminous-lightyears/src/characters/alex_the_analyst_chr.py b/luminous-lightyears/src/characters/alex_the_analyst_chr.py
new file mode 100644
index 0000000..65346b3
--- /dev/null
+++ b/luminous-lightyears/src/characters/alex_the_analyst_chr.py
@@ -0,0 +1,116 @@
+"""Alex the analyst media templates and information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Alex the Analyst", "https://i.postimg.cc/D0mxzB3d/Alex.webp",[
+ StageGroup(1, [
+ t(
+ "When the factory gets new machinery and the production line still breaks down. π€¦ββοΈπ #FactoryProblems #MondayBlues", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Employee of the month: The coffee machine. βοΈπ #UnsungHero #OfficeLife",
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Joke: Why don't scientists trust atoms? Because they make up everything! ππ¬ #ScienceJokes #Humor",
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Criticism: Our new safety equipment rollout has been slower than anticipated. Workers are concerned. π‘οΈπ #SafetyFirst #WorkplaceIssues", # noqa: E501
+ choices={
+ "Acknowledge": {"loyalty": +2, "security": +2},
+ "Ignore": {"loyalty": -1, "security": -1},
+ },
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "Meme: When you finally finish a huge project and your boss says, 'Great, now here's another one.' π
π #WorkLife #NeverEndingStory", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Criticism: There's been talk about inadequate training programs. Employees feel unprepared. ππ οΈ #TrainingIssues #WorkplaceConcerns", # noqa: E501
+ choices={
+ "Address": {"loyalty": +2},
+ "Ignore": {"loyalty": -2},
+ },
+ ),
+ t(
+ "Joke: I told my boss three companies were after me and I needed a raise. We laughed. Then I said, 'Seriously, I am going to Amazon, Google, or Netflix if you don not give me a raise.' ππΌ #JobHumor #SalaryTalks", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Positive Feedback: Great job team on hitting our production goals! ππ #TeamWork #Success",
+ choices={
+ "Like": {"loyalty": +2},
+ "Share": {"loyalty": +3},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Meme: When the coffee machine is broken and you have a full day of meetings ahead. βοΈπ #OfficeStruggles #NeedCoffee", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Meme: When you realize the 'urgent' email you sent an hour ago still has not been read. π§π© #EmailProblems #WorkFrustrations", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Criticism: Recent changes in work hours are causing stress among workers. Something needs to be done. β°π #WorkHours #EmployeeWellbeing", # noqa: E501
+ choices={
+ "Address": {"loyalty": +3, "security": +2},
+ "Ignore": {"loyalty": -3, "security": -2},
+ },
+ ),
+ t(
+ "Joke: Why did the scarecrow get a promotion? Because he was outstanding in his field! ππΎ #WorkJokes #Promotion", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "Meme: When you realize it is Friday and you have made it through another week. ππ #TGIF #WeekendVibes",
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ ),
+ ]),
+], weight=200,
+)
diff --git a/luminous-lightyears/src/characters/andy_the_athelete_chr.py b/luminous-lightyears/src/characters/andy_the_athelete_chr.py
new file mode 100644
index 0000000..26f563a
--- /dev/null
+++ b/luminous-lightyears/src/characters/andy_the_athelete_chr.py
@@ -0,0 +1,97 @@
+"""Andy character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Andy the Athlete", "https://i.postimg.cc/50h92KZ8/Andy.webp",[
+ StageGroup(1, [
+ t(
+ "Dear leader of {nation_name}, please help our climate change marathon's fundraiser.",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5, "world_opinion" : +10},
+ "Nope": {"loyalty": -5, "world_opinion":-5},
+ },
+ ),
+ t(
+ "Leader of {nation_name}, our national team needs better training facilities.",
+ choices={
+ "Approve funding": {"money": -15, "loyalty": +5},
+ "Deny funding": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Our athletes need new uniforms. Can we get them?",
+ choices={
+ "Provide new uniforms": {"money": -5, "loyalty": +2},
+ "No, use the old ones": {"loyalty": -2},
+ },
+ ),
+ t(
+ "Some athletes are requesting personal trainers. Should we approve this?",
+ choices={
+ "Approve personal trainers": {"money": -8, "loyalty": +3},
+ "Deny the request": {"loyalty": -2},
+ },
+ condition=lambda state: state.money > 50,
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "We've been invited to a major international sports event. Should we participate?",
+ choices={
+ "Yes, participate": {"money": -20, "world_opinion": +10, "loyalty": +5},
+ "No, it's too expensive": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "The national team's performance is dropping. Should we invest in a performance coach?",
+ choices={
+ "Invest in performance coach": {"money": -12, "loyalty": +5, "world_opinion": +3},
+ "No, it's unnecessary": {"loyalty": -3, "world_opinion": -2},
+ },
+ condition=lambda state: state.loyalty < 60,
+ ),
+ t(
+ "One of our top athletes has a serious injury. Should we fund their medical treatment?",
+ choices={
+ "Yes, fund treatment": {"money": -15, "loyalty": +10},
+ "No, it's too costly": {"loyalty": -10},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t("Dear leader of {nation_name}, please help our climate change marathon's fundraiser.",
+ choices = {
+ "Sure": {"money": -45, "loyalty": +5, "world_opinion" : +10},
+ "Nope": {"loyalty": -5, "world_opinion":-5},
+ },
+ ),
+ t(
+ "The marathon was good, but several people were hospitalised due to heatstroke.",
+ choices = {"Oh no!": {"money": +30, "world_opinion" : -5}},
+ condition = lambda state: state.loyalty>30 and state.money<300,
+ ),
+ t(
+ "The marathon went viral, and we were able to raise a lot of funds.",
+ choices = {"Awesome!": {"money": +70, "world_opinion" : +10}},
+ condition = lambda state: state.loyalty>50 and state.money<200,
+ ),
+ t(
+ "We have a chance to sign a sponsorship deal with a major brand. Should we proceed?",
+ choices={
+ "Yes, sign the deal": {"money": +20, "world_opinion": +10},
+ "No, it conflicts with our values": {"loyalty": +5, "world_opinion": -5},
+ },
+ condition=lambda state: state.world_opinion > 50,
+ ),
+ t(
+ "The sports facilities need renovations to meet international standards. Should we invest?",
+ choices={
+ "Invest in renovations": {"money": -25, "loyalty": +10, "world_opinion": +5},
+ "No, they are fine as they are": {"loyalty": -5, "world_opinion": -3},
+ },
+ condition=lambda state: state.money > 200,
+ ),
+ ]),
+], weight=80)
diff --git a/luminous-lightyears/src/characters/aura_the_activist_chr.py b/luminous-lightyears/src/characters/aura_the_activist_chr.py
new file mode 100644
index 0000000..0fc69ca
--- /dev/null
+++ b/luminous-lightyears/src/characters/aura_the_activist_chr.py
@@ -0,0 +1,76 @@
+"""Aura character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Aura the Activist", "https://i.postimg.cc/qvw7xL3d/aura.jpg",[
+ StageGroup(1, [
+ t(
+ "Kind leader of {nation_name}! Please donate to our LGBTQ+ movement.",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5, "world_opinion" : +10},
+ "Nope": {"loyalty": -5, "world_opinion" : -15},
+ },
+ ),
+ t(
+ "Rights are Rights! You did the right thing.",
+ choices = {"Great!": {"money": +5}},
+ condition = lambda state: state.loyalty>30,
+ ),
+ t(
+ "There's an opportunity to host a human rights seminar in our nation. Should we do it?",
+ choices={
+ "Yes, host the seminar": {"money": -8, "world_opinion": +5, "loyalty": +3},
+ "No, it's too costly": {"loyalty": -3, "world_opinion": -2},
+ },
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "One of our leading activists has been unjustly detained. Should we fund their legal defense?",
+ choices={
+ "Yes, fund legal defense": {"money": -10, "loyalty": +10},
+ "No, it's too costly": {"loyalty": -10},
+ },
+ ),
+ t(
+ "The environmental group is requesting funds to plant trees across the city. Should we approve this?",
+ choices={
+ "Approve funds": {"money": -8, "loyalty": +5, "world_opinion": +3},
+ "Deny the request": {"loyalty": -4},
+ },
+ condition=lambda state: state.loyalty > 60,
+ ),
+ t(
+ "News in someone gassed the rally.",
+ choices = {"Great!": {"money": -10, "world_opinion" : -10, "security" : -5}},
+ condition = lambda state: state.loyalty<50 and state.security<75,
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "The international community is considering us for a prestigious human rights award. Should we lobby for it?", # noqa: E501
+ choices={
+ "Yes, lobby for the award": {"money": -10, "world_opinion": +15, "loyalty": +5},
+ "No, it's not worth it": {"world_opinion": -5},
+ },
+ ),
+ t(
+ "Several community leaders are asking for funds to improve local social programs. Should we approve it?",
+ choices={
+ "Approve the funds": {"money": -20, "loyalty": +10},
+ "No, we can't afford it": {"loyalty": -5},
+ },
+ condition=lambda state: state.money > 100,
+ ),
+ t(
+ "There's a push to create stricter environmental regulations. Should we support it?",
+ choices={
+ "Yes, support the regulations": {"security": +5, "world_opinion": +10},
+ "No, it's too restrictive": {"loyalty": -5, "world_opinion": -5},
+ },
+ condition=lambda state: state.security > 50,
+ ),
+ ]),
+], weight=90)
diff --git a/luminous-lightyears/src/characters/craig_the_contractor_chr.py b/luminous-lightyears/src/characters/craig_the_contractor_chr.py
new file mode 100644
index 0000000..8b5c64a
--- /dev/null
+++ b/luminous-lightyears/src/characters/craig_the_contractor_chr.py
@@ -0,0 +1,84 @@
+"""Craig character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Craig the Contractor", "https://i.postimg.cc/fbbR29cS/Craig.webp",[
+ StageGroup(1, [
+ t(
+ "Leader of {nation_name}, the roads need mending following the heavy use.",
+ choices={
+ "Sure": {"money": -30, "loyalty": +5, "world_opinion" : +10, "security" : +10},
+ "Nope": {"loyalty": -5, "world_opinion" : -5},
+ },
+ ),
+ t(
+ "The roads are being repaired thanks to the funds provided",
+ choices = {
+ "Take more funds": {"money": -10, "loyalty" : +10},
+ "Great": {"world_opinion" : +10},
+ },
+ ),
+ t(
+ "Some contractors are requesting bonuses for their hard work. Should we approve this?",
+ choices={
+ "Approve bonuses": {"money": -8, "loyalty": +4},
+ "Deny the request": {"loyalty": -3},
+ },
+ condition=lambda state: state.money > 50,
+ ),
+ ]),
+ StageGroup(2, [
+ t("Leader of {nation_name}, we must upgrade our old government buildings.",
+ choices = {
+ "Sure": {"money": -20, "loyalty": +5},
+ "Nope": {"loyalty": -5, "world_opinion" : -10, "security" : -5},
+ },
+ ),
+ t(
+ "We have shown great strides in the redevelopment plan, would you hold the inaguration ceremony?",
+ choices = {"Yes!": {"money": +5, "loyalty":+10, "world_opinion" : +10},
+ "No!" : { "loyalty" : -5}},
+ condition = lambda state: state.loyalty>40,
+ ),
+ t(
+ "We've been asked to build a new hospital in a rural area. Should we take on this project?",
+ choices={
+ "Yes, build the hospital": {"money": -20, "security": +10, "loyalty": +5},
+ "No, it's too expensive": {"security": -5, "loyalty": -3},
+ },
+ ),
+ t(
+ "The construction team suggests a new eco-friendly building method. Should we adopt it?",
+ choices={
+ "Adopt the new method": {"money": -10, "world_opinion": +10},
+ "Stick to old methods": {"world_opinion": -5},
+ },
+ condition=lambda state: state.world_opinion > 50,
+ ),
+ ]),
+ StageGroup(3, [
+ t("Leader of {nation_name}, the drains need to be declogged following the flood.",
+ choices = {
+ "Sure": {"money": -40, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ ),
+ t(
+ "The international community is considering us for a prestigious construction award. Should we lobby for it?", # noqa: E501
+ choices={
+ "Yes, lobby for the award": {"money": -10, "world_opinion": +15, "loyalty": +5},
+ "No, it's not worth it": {"world_opinion": -5},
+ },
+ ),
+ t(
+ "Several community leaders are asking for funds to improve local infrastructure. Should we approve it?",
+ choices={
+ "Approve the funds": {"money": -20, "security": +10},
+ "No, we can't afford it": {"security": -5},
+ },
+ condition=lambda state: state.money > 100,
+ ),
+ ]),
+], weight=70)
diff --git a/luminous-lightyears/src/characters/dave_the_doctor_chr.py b/luminous-lightyears/src/characters/dave_the_doctor_chr.py
new file mode 100644
index 0000000..a6e42bc
--- /dev/null
+++ b/luminous-lightyears/src/characters/dave_the_doctor_chr.py
@@ -0,0 +1,96 @@
+"""Dave the doctor character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Dave the Doctor", "https://i.postimg.cc/Y9GqkByp/Dave.webp",[
+ StageGroup(1, [
+ t(
+ "Great Leader of {nation_name}, we need funding to create vaccines for this round of seasonal flu!",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5, "world_opinion" : +5, "security" : +5},
+ "Flu is easy to ovecome": {"loyalty": -5, "security" : -10, "world_opinion" : -5},
+ },
+ ),
+ t(
+ "We have manufactured the vaccines, but lack the funding to distribute, we require your assistance on this", # noqa: E501
+ choices = {
+ "Sure": {"money": -15},
+ "Nope":{ "world_opinion" : -20, "security" : -10, "loyalty" : -10},
+ },
+ ),
+ t(
+ "Leader of {nation_name}, our hospital is overwhelmed with patients. We need additional funding to hire more staff.", # noqa: E501
+ choices={
+ "Approve funding": {"money": -20, "security": +5, "loyalty": +3},
+ "Deny funding": {"security": -5, "loyalty": -3},
+ },
+ ),
+ t(
+ "There's a flu outbreak in the neighboring regions. Should we prepare by stocking up on vaccines?",
+ choices={
+ "Yes, stock up on vaccines": {"money": -10, "security": +10, "world_opinion": +5},
+ "No, it's not necessary": {"security": -5, "world_opinion": -3},
+ },
+ ),
+ t(
+ "Some doctors are requesting additional training to improve their skills. Should we invest in their education?", # noqa: E501
+ choices={
+ "Invest in training": {"money": -10, "loyalty": +3, "security": +2},
+ "No, they are skilled enough": {"loyalty": -2, "security": -1},
+ },
+ condition=lambda state: state.money > 30,
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "There's a proposal to build a new children's hospital in a underserved area. Should we support it?",
+ choices={
+ "Yes, build the hospital": {"money": -25, "loyalty": +10, "world_opinion": +5},
+ "No, we can't afford it": {"loyalty": -5, "world_opinion": -3},
+ },
+ ),
+ t(
+ "We have the opportunity to participate in a global health initiative. Should we join?",
+ choices={
+ "Yes, join the initiative": {"money": -15, "world_opinion": +15, "security": +5},
+ "No, it's too costly": {"world_opinion": -5, "security": -3},
+ },
+ condition=lambda state: state.world_opinion > 40,
+ ),
+ t(
+ "We've been offered a partnership with a leading medical research facility. Should we accept?",
+ choices={
+ "Yes, accept the partnership": {"money": -20, "world_opinion": +10, "security": +5},
+ "No, we can manage on our own": {"world_opinion": -5, "security": -3},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "We have the chance to lead a groundbreaking health research project. Should we take the lead?",
+ choices={
+ "Lead the project": {"money": -30, "world_opinion": +15, "security": +10},
+ "No, let others lead": {"world_opinion": -5, "security": -3},
+ },
+ condition=lambda state: state.security > 50,
+ ),
+ t(
+ "A leading pharmaceutical company wants to conduct trials in our nation. Should we allow it?",
+ choices={
+ "Allow the trials": {"money": +20, "security": +5, "world_opinion": +5},
+ "Deny the trials": {"security": -5, "world_opinion": -5},
+ },
+ condition=lambda state: state.money < 50,
+ ),
+ t(
+ "The community is requesting free health check-ups. Should we organize this?",
+ choices={
+ "Yes, organize free check-ups": {"money": -15, "loyalty": +10, "world_opinion": +5},
+ "No, it's too costly": {"loyalty": -5, "world_opinion": -3},
+ },
+ condition=lambda state: state.loyalty > 50,
+ ),
+ ]),
+], weight=110)
diff --git a/luminous-lightyears/src/characters/else_the_engineer_chr.py b/luminous-lightyears/src/characters/else_the_engineer_chr.py
new file mode 100644
index 0000000..146dc1d
--- /dev/null
+++ b/luminous-lightyears/src/characters/else_the_engineer_chr.py
@@ -0,0 +1,51 @@
+"""Else character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Elsa the Engineer", "https://i.postimg.cc/kGWJ6Fmz/elsa.webp",[
+ StageGroup(1, [
+ t(
+ "Leader of {nation_name},it is my humble request allocate more budget towards research for\
+ technology used in military field",
+ choices={
+ "Sure": {"money": -30, "loyalty": +5, "security" : +15},
+ "Nope": {"loyalty": -5, "world_opinion" : -10},
+ },
+ ),
+ t(
+ "We are competiting the cutting edge military tech now.",
+ choices = {"Great!": {"security": +10}},
+ condition = lambda state: state.security<50,
+ ),
+ ]),
+ StageGroup(2, [
+ t("Leader of {nation_name}, it is my humble request allocate some of the budget\
+ in the manufacturing sector",
+ choices = {
+ "Sure": {"money": -20, "loyalty": +5, "world_opinion" : +10},
+ "Nope": {"loyalty": -5, "money" : -10},
+ },
+ ),
+ t(
+ "Now we won't be requiring to be import certain important materials.",
+ choices = {"Great!": {"money": +30}},
+ condition = lambda state: state.loyalty>40 and state.money>500,
+ ),
+ ]),
+ StageGroup(3, [
+ t("Leader of {nation_name}, it is my humble request to give subsidies for \
+ research companies in our nation",
+ choices = {
+ "Sure": {"money": -30, "loyalty": +5 , "world_opinion" : +10},
+ "Nope": {"loyalty": -5 , "world_opinion" : -10},
+ },
+ ),
+ t(
+ "Now the nation would be filing more patents.",
+ choices = {"Great!": {"money": +50}},
+ condition = lambda state: state.security>50 and state.money>500,
+ ),
+ ]),
+], weight=60)
diff --git a/luminous-lightyears/src/characters/fred_the_farmer_chr.py b/luminous-lightyears/src/characters/fred_the_farmer_chr.py
new file mode 100644
index 0000000..8c23c68
--- /dev/null
+++ b/luminous-lightyears/src/characters/fred_the_farmer_chr.py
@@ -0,0 +1,102 @@
+"""Fred character template."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Fred the Farmer", "https://i.postimg.cc/ZKcmGqK8/Fred.webp",[
+ StageGroup(1, [
+ t(
+ "Hello, leader of {nation_name}. We request you to give subsidies for seeds",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Leader of {nation_name}, our farms need new irrigation systems to increase crop yield.",
+ choices={
+ "Approve funding": {"money": -15, "security": +5, "loyalty": +3},
+ "Deny funding": {"security": -5, "loyalty": -3},
+ },
+ ),
+ t(
+ "There's an opportunity to switch to organic farming. Should we support this transition?",
+ choices={
+ "Yes, support organic farming": {"money": -10, "world_opinion": +10, "loyalty": +5},
+ "No, it's not necessary": {"world_opinion": -5, "loyalty": -3},
+ },
+ ),
+ t(
+ "Farmers are requesting subsidies to cope with recent droughts. Should we grant them?",
+ choices={
+ "Grant subsidies": {"money": -15, "loyalty": +5, "world_opinion": +3},
+ "No, we can't afford it": {"loyalty": -5, "world_opinion": -3},
+ },
+ condition=lambda state: state.money > 30,
+ ),
+ ]),
+ StageGroup(2, [
+ t("Hello, leader of {nation_name}. Can you raise the Minimum Support Price for crops",
+ choices = {
+ "Sure": {"money": -20, "loyalty": +15},
+ "Nope": {"loyalty": -10},
+ },
+ ),
+ t(
+ "There's a proposal to introduce advanced farming technologies. Should we invest in this?",
+ choices={
+ "Yes, invest in technology": {"money": -20, "security": +10, "loyalty": +5},
+ "No, it's too expensive": {"security": -5, "loyalty": -3},
+ },
+ ),
+ t(
+ "A neighboring country wants to collaborate on a farming project. Should we join them?",
+ choices={
+ "Yes, join the project": {"money": -15, "world_opinion": +10, "security": +5},
+ "No, we can manage on our own": {"world_opinion": -5, "security": -3},
+ },
+ ),
+ t(
+ "Farmers are organizing a festival to celebrate the harvest. Should we sponsor it?",
+ choices={
+ "Sponsor the festival": {"money": -10, "loyalty": +10, "world_opinion": +5},
+ "No, it's not necessary": {"loyalty": -5, "world_opinion": -3},
+ },
+ condition=lambda state: state.world_opinion > 40,
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Our nation is being considered for a prestigious agricultural award. Should we lobby for it?",
+ choices={
+ "Yes, lobby for the award": {"money": -10, "world_opinion": +20, "loyalty": +5},
+ "No, it's not worth it": {"world_opinion": -5},
+ },
+ ),
+ t(
+ "We have the chance to lead a global agricultural research project. Should we take the lead?",
+ choices={
+ "Lead the project": {"money": -25, "world_opinion": +15, "security": +10},
+ "No, let others lead": {"world_opinion": -5, "security": -3},
+ },
+ condition=lambda state: state.security > 50,
+ ),
+ t(
+ "A major agricultural firm wants to test new fertilizers in our fields. Should we allow it?",
+ choices={
+ "Allow the tests": {"money": +20, "security": +5, "world_opinion": +5},
+ "Deny the tests": {"security": -5, "world_opinion": -5},
+ },
+ condition=lambda state: state.money < 50,
+ ),
+ t(
+ "Farmers are requesting financial aid due to a recent pest infestation. Should we help them?",
+ choices={
+ "Yes, provide aid": {"money": -15, "loyalty": +10, "world_opinion": +5},
+ "No, it's too costly": {"loyalty": -5, "world_opinion": -3},
+ },
+ condition=lambda state: state.loyalty > 50,
+ ),
+ ]),
+], weight=120)
diff --git a/luminous-lightyears/src/characters/gary_the_general_chr.py b/luminous-lightyears/src/characters/gary_the_general_chr.py
new file mode 100644
index 0000000..8fa205c
--- /dev/null
+++ b/luminous-lightyears/src/characters/gary_the_general_chr.py
@@ -0,0 +1,128 @@
+"""General template and information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Gary the General", "https://i.postimg.cc/c134bZnh/gary.webp",[
+ StageGroup(1, [
+ t(
+ "Sir, ruler of {nation_name}! The army needs new helmets to improve safety!",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5, "security": +3},
+ "Nope": {"loyalty": -5, "security": -5},
+ },
+ weight=90,
+ ),
+ t(
+ "Sir, ruler of {nation_name}! The army needs new vests to improve safety!",
+ choices={
+ "Sure": {"money": -5, "loyalty": +3, "security": +2},
+ "Nope": {"loyalty": -3, "security": -2},
+ },
+ weight=110,
+ ),
+ t(
+ "Sir, we have received intelligence about potential threats from neighboring countries.",
+ choices={
+ "Increase border security": {"money": -20, "security": +10, "world_opinion": -5, "loyalty": +3},
+ "Ignore the threats": {"security": -10, "world_opinion": +5, "loyalty": -5},
+ },
+ weight=80,
+ ),
+ t(
+ "Good morning, {nation_name}! Our brave soldiers are out training hard today. πͺ #MilitaryStrength #ProudNation", # noqa: E501
+ choices={
+ "Like": {"loyalty": +2},
+ "Share": {"world_opinion": +1},
+ "Ignore": {"loyalty": -2},
+ },
+ weight=80,
+ ),
+ t(
+ "Just received new supplies for the troops. Thanks to everyone for your support! π‘οΈ #SupportOurTroops #NationalSecurity", # noqa: E501
+ choices={
+ "Comment 'Great job!'": {"loyalty": +3},
+ "Ignore": {"loyalty": -3},
+ },
+ weight=120,
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "Sir, ruler of {nation_name}! The army needs equipment Sir!",
+ choices = {
+ "Sure": {"money": -20, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ weight=150,
+ ),
+ t(
+ "Thanks Sir! Now our army is stronger than ever Sir!",
+ choices = {"Great!": {"money": +30}},
+ condition = lambda state: state.security>80 and state.money<200 and state.loyalty>70,
+ weight=150,
+ ),
+ t(
+ "Sir, we've been invited to a joint military exercise with allied nations.",
+ choices={
+ "Participate in the exercise": {"money": -25, "security": +10, "world_opinion": +7},
+ "Decline the invitation": {"world_opinion": -5, "security": -3},
+ },
+ condition = lambda state: state.security>60 and state.money>120,
+ weight=80,
+ ),
+ t(
+ "Our intelligence team has thwarted a potential threat. Kudos to them! π΅οΈββοΈ #SecurityFirst #StaySafe",
+ choices={
+ "Like": {"security": +2},
+ "Share": {"world_opinion": +2},
+ "Ignore": {"loyalty": -5},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Sir, our intelligence agency needs more resources to counter espionage.",
+ choices={
+ "Allocate resources": {"money": -40, "security": +12, "loyalty": +5},
+ "Deny the request": {"loyalty": -10, "security": -5},
+ },
+ weight=90,
+ ),
+ t(
+ "Sir, ruler of {nation_name}! The army needs weapons Sir!",
+ choices = {
+ "Sure": {"money": -40, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ weight=150,
+ ),
+ t(
+ "Sir, a major international summit on global security is approaching. Should we attend?",
+ choices={
+ "Attend the summit": {"money": -35, "world_opinion": +15, "security": +5},
+ "Skip the summit": {"world_opinion": -10, "security": -2},
+ },
+ weight=105,
+ ),
+ t(
+ "Proud to announce new advancements in our military technology. Future-ready and strong! π #TechInDefense #Innovation", # noqa: E501
+ choices={
+ "Like": {"security": +3},
+ "Comment 'Impressive!'": {"world_opinion": +3},
+ "Ignore": {"loyalty": -5, "world_opinion": -10},
+ },
+ weight=120,
+ ),
+ t(
+ "Remembering our fallen heroes today. Their sacrifice keeps us free. #RemembranceDay #NeverForget",
+ choices={
+ "Like": {"loyalty": +3},
+ "Share": {"world_opinion": +2},
+ "Ignore": {"loyalty": -5, "world_opinion": -10},
+ },
+ weight=150,
+ ),
+ ]),
+], weight=130)
diff --git a/luminous-lightyears/src/characters/national_media_chr.py b/luminous-lightyears/src/characters/national_media_chr.py
new file mode 100644
index 0000000..4e1230d
--- /dev/null
+++ b/luminous-lightyears/src/characters/national_media_chr.py
@@ -0,0 +1,84 @@
+"""National media templates and information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("National Media", "https://i.postimg.cc/VsTY8Wpz/defcord.webp", [
+ StageGroup(1, [
+ t(
+ "Trending Now: A rumor is spreading that neighboring nations are stockpiling weapons. Should we respond?",
+ choices={
+ "Yes, increase military spending": {"money": -20, "security": +5, "world_opinion": -10},
+ "No, it's just a rumor": {"security": -5, "world_opinion": +5},
+ },
+ ),
+ t(
+ "Viral Post: A celebrity is urging people to boycott local produce due to pesticide use. Should we address this?", # noqa: E501
+ choices={
+ "Yes, ban the use of pesticides": {"money": -15, "loyalty": +5, "world_opinion": +5},
+ "No, ignore the celebrity": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "Breaking News: A popular social media figure claims your administration is hiding a health crisis. Should we respond?", # noqa: E501
+ choices={
+ "Yes, launch a PR campaign": {"money": -10, "loyalty": +5, "security": +5},
+ "No, it's not true": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "Hashtag Alert: #NoMoreTaxes is trending. Citizens are protesting against the latest tax hike. Should we lower taxes?", # noqa: E501
+ choices={
+ "Yes, lower taxes": {"money": -20, "loyalty": +10, "world_opinion": +5},
+ "No, maintain the current tax rate": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "Misinformation: False reports of a natural disaster in a key agricultural region are causing panic. Should we allocate emergency funds?", # noqa: E501
+ choices={
+ "Yes, allocate emergency funds": {"money": -15, "security": +5, "world_opinion": +5},
+ "No, verify the reports first": {"security": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "Influencer Post: A famous influencer is promoting a controversial new technology. Should we adopt this technology?", # noqa: E501
+ choices={
+ "Yes, adopt the technology": {"money": -25, "loyalty": +10, "world_opinion": +5},
+ "No, it's too risky": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Trending Conspiracy: A conspiracy theory is circulating that the government is spying on citizens. Should we address this?", # noqa: E501
+ choices={
+ "Yes, deny the accusations": {"money": -10, "security": +5, "world_opinion": +5},
+ "No, ignore it": {"security": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "False Alarm: A fake news article claims that a neighboring nation is planning an attack. Should we prepare for war?", # noqa: E501
+ choices={
+ "Yes, prepare for war": {"money": -30, "security": +10, "world_opinion": -10},
+ "No, investigate the claim first": {"security": -5, "world_opinion": +5},
+ },
+ ),
+ t(
+ "Viral Challenge: A dangerous social media challenge is encouraging people to vandalize public property. Should we address this?", # noqa: E501
+ choices={
+ "Yes, launch a public awareness campaign": {"money": -15, "loyalty": +5, "world_opinion": +5},
+ "No, it's just a phase": {"loyalty": -5, "world_opinion": -5},
+ },
+ ),
+ t(
+ "Social Media Pressure: Users are demanding immediate action on climate change. Should we implement drastic measures?", # noqa: E501
+ choices={
+ "Yes, implement drastic measures": {"money": -20, "security": +5, "world_opinion": +10},
+ "No, take a more measured approach": {"security": -5, "world_opinion": -5},
+ },
+ ),
+ ]),
+], weight=200)
diff --git a/luminous-lightyears/src/characters/sam_the_scientist_chr.py b/luminous-lightyears/src/characters/sam_the_scientist_chr.py
new file mode 100644
index 0000000..f735204
--- /dev/null
+++ b/luminous-lightyears/src/characters/sam_the_scientist_chr.py
@@ -0,0 +1,133 @@
+"""Scientist template information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Sam the Scientist", "https://i.postimg.cc/xTSTr7G2/sam.webp",[
+ StageGroup(1, [
+ t(
+ "Greetings, esteemed leader of {nation_name}. May I request your consideration for a financial contribution towards our research?", # noqa: E501
+ choices={
+ "Sure": {"money": -10, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Exciting news! Our team has just published a paper on renewable energy solutions. π±π #GreenEnergy #ScientificResearch", # noqa: E501
+ choices={
+ "Like": {"world_opinion": +2},
+ "Share": {"world_opinion": +3},
+ "Ignore": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Hello, {nation_name}! Our research team needs funding to continue our groundbreaking work on renewable energy.", # noqa: E501
+ choices={
+ "Approve funding": {"money": -20, "world_opinion": +5, "security": +3},
+ "Deny funding": {"loyalty": -5},
+ },
+ ),
+ t(
+ "We've developed a new technique to increase crop yields. Should we implement it?",
+ choices={
+ "Yes, implement it": {"money": -10, "world_opinion": +3, "loyalty": +5},
+ "No, not now": {"loyalty": -3},
+ },
+ condition=lambda state: state.money > 50,
+ ),
+ t(
+ "We've been invited to collaborate on a climate change project with international scientists.",
+ choices={
+ "Join the project": {"money": -15, "world_opinion": +7},
+ "Decline": {"loyalty": -2},
+ },
+ weight=110,
+ ),
+ t(
+ "Here's a fun fact: Did you know that our new crop yield technique could increase food production by 30%? π
#Agriculture #FoodSecurity", # noqa: E501
+ choices={
+ "Like": {"loyalty": +2},
+ "Comment 'Amazing!'": {"loyalty": +3},
+ "Ignore": {"loyalty": -1},
+ },
+ condition=lambda state: state.loyalty > 30,
+ weight=150,
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "Our lab needs new equipment to stay competitive in biotech research.",
+ choices={
+ "Buy the equipment": {"money": -30, "security": +5, "loyalty": +3},
+ "Delay the purchase": {"loyalty": -4},
+ },
+ ),
+ t(
+ "We've been invited to present our findings at an international conference.",
+ choices={
+ "Attend the conference": {"money": -25, "world_opinion": +10},
+ "Decline the invitation": {"world_opinion": -5},
+ },
+ condition=lambda state: state.world_opinion > 40,
+ ),
+ t(
+ "A major corporation wants to fund our research in exchange for some control over our findings.",
+ choices={
+ "Accept the funding": {"money": +40, "loyalty": -5, "world_opinion": -3},
+ "Reject the funding": {"loyalty": +5, "world_opinion": +3},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "We've discovered a potential cure for a common disease. Should we proceed with human trials?",
+ choices={
+ "Approve the trials": {"money": -40, "world_opinion": +15, "security": +10},
+ "Delay the trials": {"loyalty": -5},
+ },
+ condition=lambda state: state.security > 60,
+ ),
+ t(
+ "A foreign government has offered to fund our research in exchange for shared knowledge.",
+ choices={
+ "Accept the offer": {"money": +50, "world_opinion": +5, "security": -10},
+ "Decline the offer": {"world_opinion": -3},
+ },
+ ),
+ t(
+ "We've made a breakthrough in AI technology that could revolutionize various industries.",
+ choices={
+ "Invest in AI": {"money": -50, "world_opinion": +20, "security": +10},
+ "Hold off for now": {"loyalty": -7},
+ },
+ ),
+ t(
+ "Today's science joke: Why did the biologist look forward to casual Fridays? Because they're allowed to wear genes! π€£ #ScienceJokes #FunFact", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {"loyalty": -5},
+ },
+ weight=200,
+ ),
+ t(
+ "We've entered a new international collaboration. Excited for the future of science! π #GlobalResearch #ScienceTogether", # noqa: E501
+ choices={
+ "Retweet": {"world_opinion": +5},
+ "Comment 'Fantastic!'": {"loyalty": +3},
+ "Ignore": {"world_opinion": -5},
+ },
+ weight=150,
+ ),
+ t(
+ "Science trivia: What's the most abundant gas in Earth's atmosphere? Nitrogen! π§ π #ScienceFacts #Trivia",
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"world_opinion": +2},
+ "Ignore": {"loyalty": -1},
+ },
+ weight=150,
+ ),
+ ]),
+], weight=120)
diff --git a/luminous-lightyears/src/characters/sandra_the_spy_chr.py b/luminous-lightyears/src/characters/sandra_the_spy_chr.py
new file mode 100644
index 0000000..87b41cd
--- /dev/null
+++ b/luminous-lightyears/src/characters/sandra_the_spy_chr.py
@@ -0,0 +1,192 @@
+"""Sandra the spy template information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Sandra the Spy", "https://i.postimg.cc/MKbZzDtj/sandra.webp",[
+ StageGroup(1, [
+ t(
+ "Leader of {nation_name}, we need additional funds to enhance our espionage network.",
+ choices={
+ "Approve funds": {"money": -15, "security": +5},
+ "Deny funds": {"security": -5},
+ },
+ ),
+ t(
+ "A rival nation is developing new technology. Should we attempt to steal their plans?",
+ choices={
+ "Yes, authorize the mission": {"money": -10, "security": +7},
+ "No, too risky": {"security": -3},
+ },
+ condition=lambda state: state.security > 30,
+ ),
+ t(
+ "We have intercepted communications from a potential threat. Should we investigate?",
+ choices={
+ "Yes, investigate": {"money": -5, "security": +5},
+ "No, ignore for now": {"security": -2},
+ },
+ ),
+ t(
+ "New security measures in place to protect our nation. Stay vigilant! π #NationalSecurity #Vigilance",
+ choices={
+ "Like": {"security": +2},
+ "Share": {"security": +3},
+ "Ignore": {"loyalty": -1},
+ },
+ weight=80,
+ ),
+ t(
+ "Top secret mission success! Our spies are the best in the world. π΅οΈββοΈβ¨ #Espionage #MissionAccomplished",
+ choices={
+ "Like": {"security": +2},
+ "Comment 'Well done!'": {"loyalty": +2},
+ "Ignore": {},
+ },
+ weight=70,
+ ),
+ t(
+ "Daily reminder: Report any suspicious activity to keep our nation safe. π¨ #SecurityAlert #StaySafe",
+ choices={
+ "Like": {"security": +1},
+ "Share": {"security": +2},
+ "Ignore": {},
+ },
+ weight=120,
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "Our informants have provided intel on an underground organization plotting against us.",
+ choices={
+ "Act on the intel": {"money": -20, "security": +10},
+ "Ignore it": {"security": -5},
+ },
+ weight=120,
+ ),
+ t(
+ "Spotted: A suspicious activity report that led to a major breakthrough. π #Security #PublicSafety",
+ choices={
+ "Like": {"security": +2},
+ "Share": {"security": +3},
+ "Ignore": {},
+ },
+ ),
+ t(
+ "A leak has been discovered within our ranks. Should we launch an internal investigation?",
+ choices={
+ "Yes, investigate": {"money": -10, "security": +5, "loyalty": -3},
+ "No, let it be": {"security": -5, "loyalty": +3},
+ },
+ weight=110,
+ ),
+ t(
+ "Public service announcement: Always stay alert and report anything unusual. π #Alertness #Security",
+ choices={
+ "Like": {"security": +1},
+ "Share": {"security": +2},
+ "Ignore": {},
+ },
+ weight=130,
+ ),
+ t(
+ "We've identified a mole within our government. Should we apprehend them?",
+ choices={
+ "Yes, apprehend": {"money": -15, "security": +10},
+ "No, watch them": {"security": -3, "loyalty": +2},
+ },
+ condition=lambda state: state.security > 60,
+ weight=130,
+ ),
+ t(
+ "We can sabotage a key project of our rival nation. Should we proceed?",
+ choices={
+ "Yes, proceed": {"money": -30, "security": +15, "world_opinion": -5},
+ "No, hold back": {"security": -5},
+ },
+ weight=80,
+ ),
+ t(
+ "Our operations have been compromised. Should we relocate our base of operations?",
+ choices={
+ "Yes, relocate": {"money": -40, "security": +20},
+ "No, stay put": {"security": -10},
+ },
+ weight=80,
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Our informants have provided intel on an underground organization plotting against us.",
+ choices={
+ "Act on the intel": {"money": -20, "security": +10},
+ "Ignore it": {"security": -5},
+ },
+ ),
+ t(
+ "Breaking: We have successfully identified and neutralized a mole. π΅οΈββοΈβ
#Espionage #NationalSecurity",
+ choices={
+ "Like": {"security": +3},
+ "Share": {"security": +4},
+ "Ignore": {},
+ },
+ condition=lambda state: state.security > 60,
+ ),
+ t(
+ "We have a chance to recruit a high-level asset from a rival nation.",
+ choices={
+ "Recruit the asset": {"money": -25, "security": +15},
+ "Pass on the opportunity": {"security": -5},
+ },
+ condition=lambda state: state.money > 50,
+ ),
+ t(
+ "Here's a spy-themed joke to lighten your day: Why did the spy stay in bed? Because he was undercover! π€£ #SpyJokes #Humor", # noqa: E501
+ choices={
+ "Like": {"loyalty": +1},
+ "Share": {"loyalty": +2},
+ "Ignore": {},
+ },
+ weight=120,
+ ),
+ t(
+ "Security tip of the day: How to recognize and report suspicious activities. π΅οΈββοΈπ #SecurityTips #PublicSafety", # noqa: E501
+ choices={
+ "Like": {"security": +1},
+ "Share": {"security": +2},
+ "Ignore": {},
+ },
+ weight=110,
+ ),
+ t(
+ "Our informants have provided intel on an underground organization plotting against us.",
+ choices={
+ "Act on the intel": {"money": -20, "security": +10},
+ "Ignore it": {"security": -5},
+ },
+ ),
+ t(
+ "We've identified a mole within our government. Should we apprehend them?",
+ choices={
+ "Yes, apprehend": {"money": -15, "security": +10},
+ "No, watch them": {"security": -3, "loyalty": +2},
+ },
+ condition=lambda state: state.security > 60,
+ ),
+ t(
+ "We can bribe a key official in a rival nation to gain access to classified information.",
+ choices={
+ "Yes, bribe them": {"money": -30, "security": +25, "world_opinion": -10},
+ "No, too dangerous": {"security": -5},
+ },
+ ),
+ t(
+ "A rogue agent has been identified. Should we neutralize them?",
+ choices={
+ "Yes, neutralize": {"money": -15, "security": +20},
+ "No, monitor them": {"security": -5, "loyalty": +3},
+ },
+ ),
+ ]),
+], weight=90)
diff --git a/luminous-lightyears/src/characters/sheela_the_singer_chr.py b/luminous-lightyears/src/characters/sheela_the_singer_chr.py
new file mode 100644
index 0000000..b52ed94
--- /dev/null
+++ b/luminous-lightyears/src/characters/sheela_the_singer_chr.py
@@ -0,0 +1,60 @@
+"""Sheel template information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Sheela the Singer", "https://i.postimg.cc/RZSShHbp/sheela.webp",[
+ StageGroup(1, [
+ t(
+ "Blessed leader of {nation_name}, please fund my public concert!",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Leader of {nation_name}, I need funding to produce a new national anthem that will boost our citizens' morale.", # noqa: E501
+ choices={
+ "Approve funding": {"money": -10, "loyalty": +5},
+ "Deny funding": {"loyalty": -5},
+ },
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "I want to start a music school to nurture local talent. It will need substantial investment.",
+ choices={
+ "Approve the school": {"money": -25, "loyalty": +15, "world_opinion": +5},
+ "Deny the project": {"loyalty": -10},
+ },
+ ),
+ t(
+ "Excited to be a guest on the popular TV show this week. Tune in! πΊπ€ #TVAppearance #Excited",
+ choices={
+ "Like": {"world_opinion": +2},
+ "Share": {"world_opinion": +3},
+ "Ignore": {},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "I've been nominated for an international music award! Thank you for all your support. ππΆ #MusicAward #FeelingBlessed", # noqa: E501
+ choices={
+ "Like": {"loyalty": +3},
+ "Share": {"world_opinion": +3},
+ "Ignore": {},
+ },
+ condition=lambda state: state.world_opinion > 50,
+ ),
+ t(
+ "Dreaming big! Planning to start a music school to nurture young talent. πΌπ #MusicSchool #FutureTalent",
+ choices={
+ "Like": {"loyalty": +3},
+ "Share": {"world_opinion": +3},
+ "Ignore": {},
+ },
+ ),
+ ]),
+])
diff --git a/luminous-lightyears/src/characters/wong_the_worker_chr.py b/luminous-lightyears/src/characters/wong_the_worker_chr.py
new file mode 100644
index 0000000..2d60aa7
--- /dev/null
+++ b/luminous-lightyears/src/characters/wong_the_worker_chr.py
@@ -0,0 +1,109 @@
+"""Wong the worker template information."""
+
+from src.templating import Actor, StageGroup
+from src.templating import ChoiceTemplate as t # noqa: N813
+
+# fmt: off
+character = Actor("Wong the Worker", "https://i.postimg.cc/ht2SdtkD/wong.webp",[
+ StageGroup(1, [
+ t(
+ "O great leader of {nation_name}. May thou be kind enough to raise our minimum wage",
+ choices={
+ "Sure": {"money": -10, "loyalty": +5},
+ "Nope": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Leader of {nation_name}, our factory needs new machinery to increase production.",
+ choices={
+ "Approve funding": {"money": -15, "loyalty": +5},
+ "Deny funding": {"loyalty": -5},
+ },
+ ),
+ t(
+ "We need better safety equipment for our workers. Can you help?",
+ choices={
+ "Provide equipment": {"money": -10, "security": +5, "loyalty": +5},
+ "Deny request": {"loyalty": -5, "security": -5},
+ },
+ ),
+ t(
+ "Excited to announce we'll be at the local job fair. Come and join our team! π·ββοΈπ #JobFair #HiringNow",
+ choices={
+ "Like": {"loyalty": +2},
+ "Share": {"loyalty": +3},
+ "Ignore": {},
+ },
+ ),
+ ]),
+ StageGroup(2, [
+ t(
+ "A nearby country is interested in a trade agreement. Should we pursue it?",
+ choices={
+ "Yes, pursue agreement": {"money": +10, "world_opinion": +5},
+ "No, it's not worth it": {"world_opinion": -5},
+ },
+ condition=lambda state: state.world_opinion > 30,
+ ),
+ t(
+ "We've had a surge in production. Should we give bonuses to the workers?",
+ choices={
+ "Yes, give bonuses": {"money": -15, "loyalty": +10},
+ "No, save the money": {"loyalty": -5},
+ },
+ ),
+ t(
+ "Welcoming foreign investment to boost our production capabilities. ππΌ #Investment #Growth",
+ choices={
+ "Like": {"world_opinion": +2},
+ "Share": {"world_opinion": +3},
+ "Ignore": {},
+ },
+ ),
+ ]),
+ StageGroup(3, [
+ t(
+ "Our competitors are starting to outpace us. Should we invest in cutting-edge technology?",
+ choices={
+ "Yes, invest": {"money": -30, "security": +10, "loyalty": +5},
+ "No, stick with what we have": {"loyalty": -5},
+ },
+ ),
+ t(
+ "We've been approached by a foreign investor. Should we accept their investment?",
+ choices={
+ "Yes, accept": {"money": +25, "world_opinion": +10},
+ "No, decline": {"world_opinion": -5},
+ },
+ ),
+ t(
+ "There's a growing unrest among workers due to long hours. Should we reduce working hours?",
+ choices={
+ "Yes, reduce hours": {"money": -10, "loyalty": +10, "security": +5},
+ "No, keep the hours": {"loyalty": -10, "security": -5},
+ },
+ ),
+ t(
+ "We've developed a new product. Should we launch it internationally?",
+ choices={
+ "Yes, launch internationally": {"money": -20, "world_opinion": +15},
+ "No, focus locally": {"world_opinion": -5},
+ },
+ ),
+ t(
+ "A new labor union is forming. Should we support its formation?",
+ choices={
+ "Yes, support it": {"loyalty": +15, "security": +5},
+ "No, oppose it": {"loyalty": -10, "security": -5},
+ },
+ ),
+ t(
+ "Launching our new product internationally! Exciting times ahead. ππ #NewProduct #GlobalLaunch",
+ choices={
+ "Like": {"world_opinion": +3},
+ "Share": {"world_opinion": +4},
+ "Ignore": {},
+ },
+ ),
+ ]),
+], weight=110)
diff --git a/luminous-lightyears/src/const.py b/luminous-lightyears/src/const.py
new file mode 100644
index 0000000..24c44ca
--- /dev/null
+++ b/luminous-lightyears/src/const.py
@@ -0,0 +1,6 @@
+"""Application constants."""
+
+error_color = (255, 58, 51)
+message_color = (124, 199, 242)
+system_message_color = (88, 245, 149)
+AFK_TIME = 60
diff --git a/luminous-lightyears/src/game.py b/luminous-lightyears/src/game.py
new file mode 100644
index 0000000..4960852
--- /dev/null
+++ b/luminous-lightyears/src/game.py
@@ -0,0 +1,210 @@
+"""Module responsible for game actions."""
+
+import asyncio
+import logging
+import random
+import time
+from typing import TYPE_CHECKING, Annotated
+
+from attrs import asdict
+from interactions import Embed, SlashContext
+
+from src.characters import all_characters
+from src.const import AFK_TIME, error_color, system_message_color
+from src.player import Player
+from src.templating import total_stages
+
+if TYPE_CHECKING:
+ from src.game_interaction import GameFactory
+ from src.templating import Stage
+
+
+GameID = str
+logger = logging.getLogger(__name__)
+
+
+class Game:
+ """Initialize a Game and it's behaviours."""
+
+ def __init__(self, id: GameID, required_no_of_players: int, game_factory: "GameFactory") -> None:
+ self.id = id
+ self.required_no_of_players: int = required_no_of_players
+ self.players: dict[Annotated[int, "discord id"], Player] = {}
+ self.stage: Stage = 1
+ self.max_time: float = random.uniform(12.5, 16)
+ self.started: bool = False
+ self.creator_id: int | None = None
+ self.game_stop_flag: bool = False
+ self.player_component_choice_mapping: dict[str, dict] = {}
+ self.game_factory: GameFactory = game_factory
+ self.values_to_check: list[str] = ["loyalty", "money", "security", "world_opinion"]
+
+ # Percentage of the time spent in the game when the next stage of the time begins (max value 1 = 100%)
+ self.cumm_percent_time_per_stage: list[float] = [0.25, 0.6, 1]
+
+ async def add_player(self, ctx: SlashContext, cmd: str = "create") -> None:
+ """Add a player to the game."""
+ logger.info(f"Adding player {ctx.user.id} to the game {self.id}")
+ if cmd == "create":
+ self.creator_id = ctx.user.id
+ player = Player(ctx, self)
+ await player.register()
+ self.players[ctx.user.id] = player
+
+ async def remove_player(self, ctx: SlashContext) -> None:
+ """Remove player from the game."""
+ player_to_delete = ctx.user.id
+
+ if player_to_delete in self.players:
+ del self.players[player_to_delete]
+
+ self.game_factory.remove_player(player_to_delete)
+
+ async def death_player(self, dead_player: Player) -> None:
+ """Mark the player as dead."""
+ embed = Embed(
+ title="We have lost a national leader in the turmoil",
+ description=f"{dead_player.state.nation_name} has lost their leadership which was done by <@{dead_player.ctx.user.id}>", # noqa: E501
+ color=system_message_color,
+ )
+
+ for key, value in asdict(dead_player.state).items():
+ embed.add_field(name=key.capitalize(), value=value)
+
+ for player in self.players.values():
+ await player.ctx.send(embed=embed, ephemeral=True)
+
+ await self.remove_player(dead_player.ctx)
+
+ if len(self.players) == 0 and self.started:
+ self.stop()
+ self.game_factory.remove_game(self.id)
+
+ async def disqualify_player(self, player: Player) -> None:
+ """Disqualify inactive player."""
+ embed = Embed(
+ title="We have lost a national leader due to inactivity.",
+ description=f"{player.state.nation_name} has lost their leadership which was done by \n <@{player.ctx.user.id}>", # noqa: E501
+ color=system_message_color,
+ )
+ for _player in self.players.values():
+ await _player.ctx.send(embed=embed, ephemeral=True)
+
+ await self.remove_player(player.ctx)
+
+ if len(self.players) == 0 and self.started:
+ self.stop()
+ self.game_factory.remove_game(self.id)
+
+ async def stop_game_by_time(self) -> None:
+ """End game because the time is up."""
+ embed = Embed(
+ title="Time Up! Game Over!",
+ description=f"Game is over! Because time is up! We have {len(self.players)} survivors! You are one of them!", # noqa: E501
+ color=system_message_color,
+ )
+
+ for player in list(self.players.values()):
+ await player.ctx.send(embed=embed, ephemeral=True)
+ await self.remove_player(player.ctx)
+
+ self.game_factory.remove_game(self.id)
+
+ async def send_stats(self) -> None:
+ """Send player stats."""
+ for player in self.players.values():
+ embed = Embed(
+ title="Stats",
+ description=f"<@{player.ctx.user.id}> current stats as follows,",
+ color=system_message_color,
+ )
+ for key, value in asdict(player.state).items():
+ embed.add_field(name=key.capitalize(), value=value)
+
+ await player.ctx.send(embed=embed, ephemeral=True)
+
+ def stop(self) -> None:
+ """Set the stop flag."""
+ self.game_stop_flag = True
+
+ async def loop(self) -> None:
+ """Define the main loop of the game."""
+ self.start_time = time.time()
+ players = self.players.values()
+ await self.send_stats()
+
+ while True:
+ logger.info(f"{len(self.players)} left in game {self.id}")
+
+ if self.game_stop_flag:
+ break
+
+ game_time: float = (time.time() - self.start_time) / 60
+ if (game_time > self.cumm_percent_time_per_stage[self.stage - 1] * self.max_time) and (
+ game_time < self.max_time
+ ):
+ self.stage = total_stages[total_stages.index(self.stage) + 1]
+ await self.send_stats()
+
+ if game_time >= self.max_time:
+ logger.info(f"Time is Up! Game {self.id} is over!")
+ self.stop()
+ await self.stop_game_by_time()
+ break
+
+ logger.info(f"{game_time=} {self.stage=} {self.max_time=}")
+
+ try:
+ response = await asyncio.gather(*[self.tick(player) for player in players], return_exceptions=True)
+
+ for res in response:
+ if isinstance(res, Exception):
+ logger.error(res)
+ except Exception as e:
+ logger.exception("Error occurred in game loop")
+
+ for player in players:
+ await player.ctx.send(
+ embed=Embed(
+ title="Some error occured",
+ description=f"{e} \n has occured, please contact the devs if you see this",
+ color=error_color,
+ ),
+ )
+
+ async def tick(self, player: Player) -> None:
+ """Define the activities done in every game tick."""
+ if self.game_stop_flag:
+ return
+
+ if (time.time() - player.last_activity_time) > AFK_TIME:
+ await self.disqualify_player(player)
+ return
+
+ character = all_characters.get_random(player.state)
+ for attr in self.values_to_check:
+ if getattr(player.state, attr) <= 0:
+ # Some value is negative hence need to send the losing message
+ await self.death_player(player)
+ return
+
+ # The sleep times are subject to change, based on how the actual gameplay feels
+ # The randomness gives a variability between the values mentioned in the brackets
+ match self.stage:
+ case 1:
+ sleep_time = 10 + (random.uniform(-2, 2))
+ case 2:
+ sleep_time = 8 + (random.uniform(-2, 1.5))
+ case 3:
+ sleep_time = 6 + (random.uniform(-1, 0.75))
+
+ character = all_characters.get_random(player.state)
+ while self.stage not in character.stages:
+ character = all_characters.get_random(player.state)
+
+ result = await character.send(player)
+
+ if result:
+ await asyncio.sleep(sleep_time)
+ else:
+ await asyncio.sleep(0.2)
diff --git a/luminous-lightyears/src/game_interaction.py b/luminous-lightyears/src/game_interaction.py
new file mode 100644
index 0000000..1856106
--- /dev/null
+++ b/luminous-lightyears/src/game_interaction.py
@@ -0,0 +1,258 @@
+"""Module responsible for game interaction with discord."""
+
+import random
+import time
+from string import ascii_uppercase, digits
+from typing import TYPE_CHECKING
+
+from interactions import Embed, Extension, OptionType, SlashContext, listen, slash_command, slash_option
+from interactions.api.events import Component
+
+from src.const import system_message_color
+from src.game import Game, GameID
+
+if TYPE_CHECKING:
+ from interactions import Client
+
+
+class GameFactory:
+ """Game creator factory class."""
+
+ def __init__(self) -> None:
+ self.games: dict[GameID, Game] = {}
+ self.players: dict[int, Game] = {}
+
+ def generate_game_id(self) -> GameID:
+ """Generate a random id for the game."""
+ while True:
+ game_id = "".join(random.choice(ascii_uppercase + digits) for i in range(6))
+
+ if game_id in self.games:
+ continue
+
+ return game_id
+
+ def create_game(self, required_no_of_players: int) -> Game:
+ """Create a game with the required details."""
+ game_id = self.generate_game_id()
+ game = Game(game_id, required_no_of_players, self)
+ self.games[game_id] = game
+
+ return game
+
+ def add_player(self, player_id: int, game: Game) -> None:
+ """Map a player with a game."""
+ self.players[player_id] = game
+
+ def remove_player(self, player_id: int) -> None:
+ """Remove a player game mapping."""
+ if player_id in self.players:
+ del self.players[player_id]
+
+ def remove_game(self, game_id: int) -> None:
+ """Remove a game from game id mapping."""
+ if game_id in self.games:
+ del self.games[game_id]
+
+ def query_game(self, game_id: GameID | None = None, player_id: int | None = None) -> Game | None:
+ """Return the game based on the query details."""
+ if game_id:
+ return self.games.get(game_id, None)
+ if player_id:
+ return self.players.get(player_id, None)
+
+ err = "You need to specify either game_id or player_id"
+ raise AttributeError(err)
+
+
+class GameInteraction(Extension):
+ """Control the extension entry point."""
+
+ def __init__(self, _: "Client") -> None:
+ self.game_factory = GameFactory()
+
+ async def send_player_join_notification(self, game: Game, ctx: SlashContext) -> None:
+ """Send a notification to all the players that a player has joined the game."""
+ for player in game.players.values():
+ remaining_players_count = game.required_no_of_players - len(game.players)
+ if remaining_players_count:
+ count_message = f"{remaining_players_count} yet to join."
+ else:
+ count_message = "All aboard. The game creator can start the game now."
+ await player.ctx.send(
+ embed=Embed(
+ title="A player joined the game",
+ description=f"<@{ctx.user.id}> joined the game.\n{count_message}",
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+
+ @slash_command(name="defcord", description="Interact with defcord.")
+ async def defcord(self, ctx: SlashContext) -> None:
+ """Make the subcommand work, since it requires a function to latch off of."""
+
+ @defcord.subcommand(sub_cmd_name="create", sub_cmd_description="Create a game of Defcord")
+ @slash_option(
+ "required_no_of_players",
+ "Number of players required for the game",
+ required=True,
+ opt_type=OptionType.INTEGER,
+ min_value=2,
+ max_value=10,
+ )
+ async def create(self, ctx: SlashContext, required_no_of_players: int = 5) -> None:
+ """Create a game of DEFCORD."""
+ existing_game = self.game_factory.query_game(player_id=ctx.user.id)
+ if existing_game:
+ await ctx.send(
+ embed=Embed(
+ title="You have already joined a game",
+ description=f"<@{ctx.user.id}> You are already part of the game with invite {existing_game.id}",
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+ return
+
+ game = self.game_factory.create_game(required_no_of_players)
+ self.game_factory.add_player(ctx.user.id, game)
+ await game.add_player(ctx, cmd="create")
+
+ embed = Embed(
+ title="New game started!",
+ description=f"Your invite: {game.id}",
+ color=system_message_color,
+ )
+
+ await ctx.send(embed=embed)
+ await self.send_player_join_notification(game, ctx)
+
+ @defcord.subcommand(sub_cmd_name="join", sub_cmd_description="Join a game of Defcord")
+ @slash_option("invite", "The invite code for the game", required=True, opt_type=OptionType.STRING)
+ async def join(self, ctx: SlashContext, invite: str) -> None:
+ """Join a game of DEFCORD."""
+ game: Game = self.game_factory.query_game(game_id=invite)
+ description: str = ""
+
+ if game is None:
+ description = f"<@{ctx.user.id}> Invite({invite}) is invalid"
+ elif ctx.user.id in game.players:
+ description = f"<@{ctx.user.id}> You are already part of the game with {invite=}"
+ elif game.required_no_of_players == len(game.players):
+ description = f"<@{ctx.user.id}> Game is already full {invite=}"
+ elif game.started:
+ description = f"<@{ctx.user.id}> Game already started"
+
+ if description != "":
+ await ctx.send(
+ embed=Embed(
+ title="Unable to join the game",
+ description=description,
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+ return
+
+ self.game_factory.add_player(ctx.user.id, game)
+ await game.add_player(ctx, cmd="join")
+ await self.send_player_join_notification(game, ctx)
+
+ @defcord.subcommand(sub_cmd_name="leave", sub_cmd_description="Leave a game of Defcord")
+ async def leave(self, ctx: SlashContext) -> None:
+ """Leave the current game of defcord."""
+ game = self.game_factory.query_game(player_id=ctx.user.id)
+
+ if game is None:
+ await ctx.send(
+ embed=Embed(
+ title="You cannot leave any game since",
+ description=f"<@{ctx.user.id}> You are not part of any game",
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+ return
+
+ await game.remove_player(ctx)
+
+ embed = Embed(
+ title="A Player left the game",
+ description=f"<@{ctx.user.id}> has left the game ({len(game.players)} players left).",
+ color=system_message_color,
+ )
+ for player in game.players.values():
+ await player.ctx.send(embed=embed, ephemeral=True)
+
+ await ctx.send(embed=embed, ephemeral=True)
+
+ if len(game.players) == 0 and game.started:
+ await ctx.send(
+ embed=Embed(
+ title="Game Over",
+ description="Game Over! You are the only one survivor. Everyone quit!",
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+ game.stop()
+ self.game_factory.remove_game(game.id)
+
+ @defcord.subcommand(sub_cmd_name="start", sub_cmd_description="Start a game of DEFCORD.")
+ async def start(self, ctx: SlashContext) -> None:
+ """Start and runs the Defcord game loop."""
+ game = self.game_factory.query_game(player_id=ctx.user.id)
+ description: str = ""
+
+ if game is None:
+ description = f"<@{ctx.user.id}> You are not part of any game"
+ elif game.creator_id != ctx.user.id:
+ description = f"<@{ctx.user.id}> Only game creator can start it"
+ elif game.started:
+ description = f"<@{ctx.user.id}> Game already started"
+ elif game.required_no_of_players != len(game.players):
+ description = f"<@{ctx.user.id}> Cannot start the game until all the players join"
+
+ if description != "":
+ ctx.send(
+ embed=Embed(
+ title="Unable to start the game",
+ description=description,
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+ return
+
+ game.started = True
+ await ctx.send(
+ embed=Embed(
+ title="Game Start",
+ description=f"<@{ctx.user.id}> Game started",
+ color=system_message_color,
+ ),
+ ephemeral=True,
+ )
+
+ for player in game.players.values():
+ player.last_activity_time = time.time()
+
+ await game.loop()
+
+ @listen(Component)
+ async def on_component(self, event: Component) -> None:
+ """Listen to button clicks."""
+ ctx = event.ctx
+ game = self.game_factory.query_game(player_id=ctx.user.id)
+
+ if not game:
+ await ctx.edit_origin(content="Your game has already ended.", components=[])
+ return
+
+ consequences = game.player_component_choice_mapping[ctx.custom_id]
+ player = game.players[ctx.user.id]
+ player.state.apply(consequences)
+ player.last_activity_time = time.time()
+ await ctx.edit_origin(content=f"Your response ({ctx.component.label}) saved.", components=[])
+ del game.player_component_choice_mapping[ctx.custom_id]
diff --git a/luminous-lightyears/src/player.py b/luminous-lightyears/src/player.py
new file mode 100644
index 0000000..d4da3d1
--- /dev/null
+++ b/luminous-lightyears/src/player.py
@@ -0,0 +1,67 @@
+from typing import TYPE_CHECKING
+import time
+from attrs import define
+from interactions import Modal, ModalContext, ShortText, SlashContext
+
+if TYPE_CHECKING:
+ from src.game import Game
+
+
+@define
+class PlayerState:
+ """The current state of the player."""
+
+ nation_name: str
+
+ # Money with the government
+ money: float = 100
+
+ # How loyal people feel to the current government that you have created
+ loyalty: float = 50
+
+ # How vulnerable is the country from external threats
+ security: float = 50
+
+ # Lower means entity sabotage and vice versa (might add this as a later future)
+ world_opinion: float = 50
+
+ def apply(self, consequence: dict) -> None:
+ """Apply the consequnces to current state."""
+ for k, v in consequence.items():
+ setattr(self, k, getattr(self, k, None) + v)
+
+
+class Player:
+ def __init__(self, ctx: SlashContext, game: "Game") -> None:
+ self.ctx: SlashContext = ctx
+ self.state: PlayerState = None # type: ignore TODO: properly type that state isn't none after register
+ self.game: Game= game
+ self.last_activity_time: float = 0
+ self.component_id: int = 0
+
+ def get_component_id(self) -> int:
+ """Return an id to be used in a component like button."""
+ self.component_id += 1
+ return self.component_id
+
+ async def register(self) -> None:
+ """Ask the player for information."""
+ registration_modal = Modal(
+ ShortText(
+ label="Provide your nation name",
+ custom_id="nation_name",
+ min_length=3,
+ max_length=50,
+ required=True,
+ ),
+ title="Player Information",
+ )
+ await self.ctx.send_modal(modal=registration_modal)
+
+ modal_ctx: ModalContext = await self.ctx.bot.wait_for_modal(registration_modal)
+
+ nation_name = modal_ctx.responses["nation_name"]
+
+ await modal_ctx.send(f"<@{self.ctx.user.id}> You are playing as a leader of {nation_name}", ephemeral=True)
+
+ self.state = PlayerState(nation_name)
diff --git a/luminous-lightyears/src/templating.py b/luminous-lightyears/src/templating.py
new file mode 100644
index 0000000..c2098ee
--- /dev/null
+++ b/luminous-lightyears/src/templating.py
@@ -0,0 +1,155 @@
+"""Utils for creating and using templates."""
+
+import logging
+import os
+from collections.abc import Callable
+from typing import TYPE_CHECKING, Any, Literal, get_args
+
+from attrs import asdict, field, frozen
+from interactions import ActionRow, Button, ButtonStyle, Embed
+
+from src.const import message_color
+from src.weighted_random import WeightedList
+
+if TYPE_CHECKING:
+ from src.player import Player, PlayerState
+
+Consequence = dict[Any, Any]
+Condition = Callable[["PlayerState"], bool] | None
+Stage = Literal[1, 2, 3] # Adjustable
+
+logger = logging.getLogger(__name__)
+
+
+@frozen
+class Template:
+ """Make a template for the messages to be served."""
+
+ text: str = field()
+ weight: int = 100
+ condition: Condition | None = None
+
+ def format(self, state: "PlayerState") -> str:
+ """Format the text."""
+ return self.text.format(**asdict(state))
+
+ def is_available(self, state: "PlayerState") -> bool:
+ """Check whether the template is available to serve."""
+ if self.condition is not None:
+ return self.condition(state)
+
+ return True
+
+ def to_embed(self, player: "Player", actor: "Actor") -> Embed:
+ """Get an embed for UI."""
+ # Now you can access actor here
+ return Embed(
+ title=f"{actor.name} of {player.state.nation_name}",
+ description=self.format(player.state),
+ color=message_color,
+ thumbnail=None if os.environ.get("WITHOUT_ACTOR_THUMBNAIL") else actor.picture,
+ )
+
+ async def ui(self, player: "Player", actor: "Actor") -> None:
+ """Send template data to ui."""
+ await player.ctx.send(embed=self.to_embed(player, actor), ephemeral=True)
+
+
+def not_none(var: Any | None) -> Any: # noqa: ANN401 temporary workaround FIXME
+ """Workaround for none check."""
+ if var is None:
+ raise AttributeError
+
+ return var
+
+
+@frozen
+class ChoiceTemplate(Template):
+ """Make a template for the messages to be served."""
+
+ choices: dict[str, Consequence] = field(default=None, converter=not_none) # Specify button color here somehow.
+
+ async def ui(self, player: "Player", actor: "Actor") -> None:
+ """Send UI and apply consequences."""
+ buttons: list[Button] = []
+
+ for _, choice in enumerate(self.choices.items()):
+ button_custom_id = f"{player.ctx.user.id}_{player.get_component_id()}"
+ player.game.player_component_choice_mapping[button_custom_id] = choice[1]
+ button = Button(
+ label=f"{choice[0]}",
+ style=ButtonStyle.BLURPLE,
+ custom_id=f"{button_custom_id}",
+ )
+ buttons.append(button)
+
+ embed = self.to_embed(player, actor)
+
+ await player.ctx.send(embed=embed, components=ActionRow(*buttons), ephemeral=True)
+
+
+total_stages = get_args(Stage)
+
+
+@frozen
+class StageGroup:
+ """A helper class to group templates based on their stage in game."""
+
+ @staticmethod
+ def convert_stage(stage: Stage | list[Stage] | Literal["all"]) -> list[Stage]:
+ """Conver stage into a required a data type."""
+ if stage == "all":
+ return list(total_stages)
+ if isinstance(stage, int):
+ return [stage]
+
+ return stage
+
+ stage: Stage | list[Stage] | Literal["all"] = field(converter=convert_stage)
+ templates: list[Template]
+
+
+@frozen
+class Actor:
+ """Respresents an actor who asks/gives information to the leader."""
+
+ @staticmethod
+ def cast_stages(stage_groups: list[StageGroup]) -> dict[Stage, WeightedList[Template]]:
+ """Cast stages into a weighted list."""
+ stages: dict[Stage, WeightedList[Template]] = {}
+
+ for stage_slot in total_stages:
+ stage_templates = []
+
+ for stage_group in stage_groups:
+ if stage_slot in stage_group.stage:
+ stage_templates += stage_group.templates
+
+ stages[stage_slot] = WeightedList(stage_templates)
+
+ return stages
+
+ name: str
+ picture: str
+ stages: dict[Stage, WeightedList[Template]] = field(converter=cast_stages)
+ weight: int = 100
+
+ def is_available(self, state: "PlayerState") -> bool:
+ """Send always available."""
+ # Add stuff here if you want to add actors which appear on condition.
+ _ = state
+ return True
+
+ async def send(self, target: "Player") -> None:
+ """Send template to discord ui."""
+ stage = self.stages[target.game.stage]
+ template = stage.get_random(target.state)
+ sent = False
+
+ if template:
+ sent = True
+ await template.ui(target, self)
+ else:
+ logger.info(f"Template not available for {stage=} in {self.name}")
+
+ return sent
diff --git a/luminous-lightyears/src/weighted_random.py b/luminous-lightyears/src/weighted_random.py
new file mode 100644
index 0000000..c35387a
--- /dev/null
+++ b/luminous-lightyears/src/weighted_random.py
@@ -0,0 +1,51 @@
+"""Weight random list implementation."""
+
+import random
+from typing import TYPE_CHECKING, Generic, Protocol, TypeVar
+
+if TYPE_CHECKING:
+ from src.player import PlayerState
+
+
+class SupportedRandomValue(Protocol):
+ """Generic type for supported classes."""
+
+ @property
+ def weight(self) -> int: ... # noqa: D102
+
+ def is_available(self, state: "PlayerState") -> bool: ... # noqa: D102
+
+
+T = TypeVar("T", bound=SupportedRandomValue)
+
+
+class WeightedList(Generic[T]):
+ """Responsible for storing and retrieving values based on weights."""
+
+ def __init__(self, values: list[T] | None = None) -> None:
+ if values is not None:
+ self.values = values
+ self.weights = [value.weight for value in values]
+ else:
+ self.values = []
+ self.weights = []
+
+ def append(self, value: T) -> None:
+ """Add a value to the weighted list."""
+ self.values.append(value)
+ self.weights.append(value.weight)
+
+ def get_random(self, state: "PlayerState") -> T:
+ """Get a random value based on the weights."""
+ result = None
+
+ if not self.values:
+ return result
+
+ while result is None:
+ possible_result = random.choices(self.values, weights=self.weights, k=1)[0]
+
+ if possible_result.is_available(state):
+ result = possible_result
+
+ return result