first commit
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | venv/* | ||||||
|  | .directory | ||||||
|  | *.pyc | ||||||
|  | __pycache__/ | ||||||
|  | .mypy_cache/ | ||||||
|  | cromulant.db | ||||||
							
								
								
									
										18
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | repos: | ||||||
|  | -   repo: https://github.com/pre-commit/mirrors-mypy | ||||||
|  |     rev: v1.11.0 | ||||||
|  |     hooks: | ||||||
|  |     -   id: mypy | ||||||
|  |         files: "^cromulant/.*" | ||||||
|  |         args: [--strict, --strict, --strict, cromulant/main.py] | ||||||
|  | -   repo: https://github.com/charliermarsh/ruff-pre-commit | ||||||
|  |     rev: v0.5.5 | ||||||
|  |     hooks: | ||||||
|  |     -   id: ruff | ||||||
|  |         name: ruff check | ||||||
|  |         entry: ruff check | ||||||
|  |         files: ^cromulant/ | ||||||
|  |     -   id: ruff | ||||||
|  |         name: ruff format | ||||||
|  |         entry: ruff format | ||||||
|  |         files: ^cromulant/ | ||||||
							
								
								
									
										118
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,118 @@ | |||||||
|  | # Cromulant | ||||||
|  |  | ||||||
|  | <img src="cromulant/img/status.jpg" width="200"> | ||||||
|  |  | ||||||
|  | [Click here for screenshots](screenshots.md) | ||||||
|  |  | ||||||
|  | ## What is this? | ||||||
|  |  | ||||||
|  | This is a kind of toy you can use for your amusement. | ||||||
|  |  | ||||||
|  | It requires minimal interaction, most things happen automatically. | ||||||
|  |  | ||||||
|  | You might want to keep it running in some tiled layout. | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | You start with a set of `25` to `250` random ants (`100` by default). | ||||||
|  |  | ||||||
|  | You can specify this anytime through `Restart`. When you restart everything resets to zero like triumphs and hits. | ||||||
|  |  | ||||||
|  | There are `1000` names available. This is used as the pool of names to select randomly. | ||||||
|  |  | ||||||
|  | Every x minutes or seconds a new update from a random ant appears. | ||||||
|  |  | ||||||
|  | The content of the update depends on a random number. | ||||||
|  |  | ||||||
|  | It can be a triumph, a hit, travel, thought, sentence. | ||||||
|  |  | ||||||
|  | The ant with the highest score is shown in the footer. | ||||||
|  |  | ||||||
|  | Ants get merged and replaced over time. | ||||||
|  |  | ||||||
|  | All of this happens automatically, though you can manually force actions | ||||||
|  | by using the mouse on the portraits or main menu. Try click and middle click. | ||||||
|  |  | ||||||
|  | Read [Algorithm](#algorithm) for more information about the mechanics. | ||||||
|  |  | ||||||
|  | ## Installation | ||||||
|  |  | ||||||
|  | First make sure you have `qt` installed in your system. | ||||||
|  |  | ||||||
|  | In arch you can do this with: `sudo pacman -S qt6-base` | ||||||
|  |  | ||||||
|  | In ubuntu/debian you can do this with: `sudo apt install qt6-base-dev` | ||||||
|  |  | ||||||
|  | ### Quick Installation | ||||||
|  |  | ||||||
|  | If you have `pipx` and `linux` installed you can use the following command: | ||||||
|  |  | ||||||
|  | ```sh | ||||||
|  | pipx install git+https://github.com/madprops/cromulant --force | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Advanced Installation | ||||||
|  |  | ||||||
|  | 1) Clone this repo. | ||||||
|  |  | ||||||
|  | 2) python -m venv venv | ||||||
|  |  | ||||||
|  | 3) venv/bin/pip install -r requirements.txt | ||||||
|  |  | ||||||
|  | 4) Use `run.sh` or `venv/bin/python -m cromulant.main` | ||||||
|  |  | ||||||
|  | 5) (Optional) Manually create desktop entries and icons for the application. | ||||||
|  |  | ||||||
|  | ## Algorithm <a name="algorithm"></a> | ||||||
|  |  | ||||||
|  | A random ant is picked based on weights (oldest update date weighs more). | ||||||
|  | More weight means something is more likely to get picked. | ||||||
|  | Then a random number between 0 and length-of-methods-1 is picked. | ||||||
|  | For each number an action happens to produce an update. | ||||||
|  |  | ||||||
|  | Words (sentences) have more weight compared to the rest of the update methods. | ||||||
|  | Some methods roll another number to pick the outcome like in the case of `think` | ||||||
|  | where there are 3 `think` types, these can also have custom weights. | ||||||
|  |  | ||||||
|  | The top score is calculated on every new update. | ||||||
|  | The score is calculated as (Triumph - Hits). | ||||||
|  | If multiple ants have the same score, the oldest one wins. | ||||||
|  | The ant with the top score is shown in the footer. | ||||||
|  | The top ant uses a special portrait on updates. | ||||||
|  |  | ||||||
|  | For merge, the words of each name are used. | ||||||
|  | They get filled with random words if less than 2 words. | ||||||
|  | One word from each set is picked randomly. | ||||||
|  | The triumph and hits from each ant get combined for the new ant. | ||||||
|  | The original ants get terminated and the merged one hatches. | ||||||
|  | An extra random ant is hatched to fill the gap. | ||||||
|  |  | ||||||
|  | ## Storage | ||||||
|  |  | ||||||
|  | The state of ants is stored in `~/.local/share/cromulant/ants.json` | ||||||
|  |  | ||||||
|  | The settings file is stored in `~/.config/cromulant/settings.json` | ||||||
|  |  | ||||||
|  | Or the equivalents in non-linux systems. | ||||||
|  |  | ||||||
|  | There is a command line argument to define a custom location for the ants state file. | ||||||
|  | This means you can have multiple states to save/load. | ||||||
|  |  | ||||||
|  | There is a command line argument to define a custom location for the names list. | ||||||
|  | This means you can use this with another set of names. | ||||||
|  | If not enough names are provided the remaining ants are created with random words. | ||||||
|  |  | ||||||
|  | ## The Name | ||||||
|  |  | ||||||
|  | I read the word [cromulent](https://www.merriam-webster.com/wordplay/what-does-cromulent-mean) being used somewhere which turned out to be invented by The Simpsons. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | I created a new programming project to practice/study and tried to use that word for the name but made a typo. | ||||||
|  |  | ||||||
|  | I liked the typo and made a game around it. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [Command line arguments](arguments.md) | ||||||
|  |  | ||||||
|  | [Click here for more](more.md) | ||||||
							
								
								
									
										189
									
								
								arguments.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,189 @@ | |||||||
|  | # Arguments | ||||||
|  |  | ||||||
|  | Here are all the available command line arguments: | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### version | ||||||
|  |  | ||||||
|  | Check the version of the program | ||||||
|  |  | ||||||
|  | Action: version | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### names | ||||||
|  |  | ||||||
|  | Path to a JSON file with a list of names. Use these instead of the default ones | ||||||
|  |  | ||||||
|  | Type: str | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### ants | ||||||
|  |  | ||||||
|  | Path to a JSON file with ants data. Use this instead of the default one | ||||||
|  |  | ||||||
|  | Type: str | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### no-images | ||||||
|  |  | ||||||
|  | Don't show the images on the left | ||||||
|  |  | ||||||
|  | Action: store_false | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### no-header | ||||||
|  |  | ||||||
|  | Don't show the header controls | ||||||
|  |  | ||||||
|  | Action: store_false | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### no-footer | ||||||
|  |  | ||||||
|  | Don't show the footer controls | ||||||
|  |  | ||||||
|  | Action: store_false | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### no-intro | ||||||
|  |  | ||||||
|  | Don't show the intro message | ||||||
|  |  | ||||||
|  | Action: store_false | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### title | ||||||
|  |  | ||||||
|  | Custom title for the window | ||||||
|  |  | ||||||
|  | Default: [Empty string] | ||||||
|  |  | ||||||
|  | Type: str | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### width | ||||||
|  |  | ||||||
|  | The width of the window in pixels | ||||||
|  |  | ||||||
|  | Default: 0 | ||||||
|  |  | ||||||
|  | Type: int | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### height | ||||||
|  |  | ||||||
|  | The height of the window in pixels | ||||||
|  |  | ||||||
|  | Default: 0 | ||||||
|  |  | ||||||
|  | Type: int | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### program | ||||||
|  |  | ||||||
|  | The internal name of the program | ||||||
|  |  | ||||||
|  | Default: [Empty string] | ||||||
|  |  | ||||||
|  | Type: str | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### speed | ||||||
|  |  | ||||||
|  | Use this update speed | ||||||
|  |  | ||||||
|  | Default: [Empty string] | ||||||
|  |  | ||||||
|  | Choices: "fast", "normal", "slow", "paused" | ||||||
|  |  | ||||||
|  | Type: str | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### clean | ||||||
|  |  | ||||||
|  | Start with clean ants data | ||||||
|  |  | ||||||
|  | Default: False | ||||||
|  |  | ||||||
|  | Action: store_true | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### fast-seconds | ||||||
|  |  | ||||||
|  | The number of seconds between fast updates | ||||||
|  |  | ||||||
|  | Default: 0 | ||||||
|  |  | ||||||
|  | Type: int | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### normal-minutes | ||||||
|  |  | ||||||
|  | The number of minutes between normal updates | ||||||
|  |  | ||||||
|  | Default: 0.0 | ||||||
|  |  | ||||||
|  | Type: float | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### slow-minutes | ||||||
|  |  | ||||||
|  | The number of minutes between slow updates | ||||||
|  |  | ||||||
|  | Default: 0.0 | ||||||
|  |  | ||||||
|  | Type: float | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### argdoc | ||||||
|  |  | ||||||
|  | Make the arguments document and exit | ||||||
|  |  | ||||||
|  | Default: False | ||||||
|  |  | ||||||
|  | Action: store_true | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### score | ||||||
|  |  | ||||||
|  | Show the score on triumph or hits instead of the total of each | ||||||
|  |  | ||||||
|  | Default: False | ||||||
|  |  | ||||||
|  | Action: store_true | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### mono | ||||||
|  |  | ||||||
|  | Use a monospace font | ||||||
|  |  | ||||||
|  | Default: False | ||||||
|  |  | ||||||
|  | Action: store_true | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### no-fade | ||||||
|  |  | ||||||
|  | Don't apply a fade-in effect on new updates | ||||||
|  |  | ||||||
|  | Action: store_false | ||||||
							
								
								
									
										0
									
								
								cromulant/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										324
									
								
								cromulant/ants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,324 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import re | ||||||
|  | import random | ||||||
|  | import itertools | ||||||
|  | from typing import ClassVar, Any | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .args import Args | ||||||
|  | from .utils import Utils | ||||||
|  | from .storage import Storage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Ant: | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         now = Utils.now() | ||||||
|  |         self.created = now | ||||||
|  |         self.updated = now | ||||||
|  |         self.name = "" | ||||||
|  |         self.status = "" | ||||||
|  |         self.method = "hatched" | ||||||
|  |         self.triumph = 0 | ||||||
|  |         self.hits = 0 | ||||||
|  |  | ||||||
|  |     def to_dict(self) -> dict[str, Any]: | ||||||
|  |         return { | ||||||
|  |             "created": self.created, | ||||||
|  |             "updated": self.updated, | ||||||
|  |             "name": self.name, | ||||||
|  |             "status": self.status, | ||||||
|  |             "method": self.method, | ||||||
|  |             "hits": self.hits, | ||||||
|  |             "triumph": self.triumph, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def from_dict(self, data: dict[str, Any]) -> None: | ||||||
|  |         self.created = data["created"] | ||||||
|  |         self.updated = data["updated"] | ||||||
|  |         self.name = data["name"] | ||||||
|  |         self.status = data["status"] | ||||||
|  |         self.method = data["method"] | ||||||
|  |         self.hits = data["hits"] | ||||||
|  |         self.triumph = data["triumph"] | ||||||
|  |  | ||||||
|  |     def get_name(self) -> str: | ||||||
|  |         return self.name or "Nameless" | ||||||
|  |  | ||||||
|  |     def get_age(self) -> str: | ||||||
|  |         now = Utils.now() | ||||||
|  |         return Utils.time_ago(self.created, now) | ||||||
|  |  | ||||||
|  |     def describe(self) -> None: | ||||||
|  |         Utils.print(f"Name is {self.get_name()}") | ||||||
|  |         Utils.print(f"It hatched {self.get_age()}") | ||||||
|  |  | ||||||
|  |     def get_score(self) -> int: | ||||||
|  |         return self.triumph - self.hits | ||||||
|  |  | ||||||
|  |     def tooltip(self) -> str: | ||||||
|  |         tooltip = "" | ||||||
|  |         tooltip += f"Updated: {Utils.to_date(self.updated)}" | ||||||
|  |         tooltip += f"\nCreated: {Utils.to_date(self.created)}" | ||||||
|  |         tooltip += f"\nTriumph: {self.triumph} | Hits: {self.hits}" | ||||||
|  |         tooltip += "\nClick to Terminate" | ||||||
|  |         tooltip += "\nMiddle Click to Merge" | ||||||
|  |         return tooltip | ||||||
|  |  | ||||||
|  |     def get_status(self) -> str: | ||||||
|  |         from .game import Method | ||||||
|  |  | ||||||
|  |         if (not self.status) and (not self.method): | ||||||
|  |             return "No update yet" | ||||||
|  |  | ||||||
|  |         status = self.status | ||||||
|  |  | ||||||
|  |         if self.method == Method.triumph: | ||||||
|  |             if Args.score: | ||||||
|  |                 total = f"(Score: {self.get_score()})" | ||||||
|  |             else: | ||||||
|  |                 total = f"({self.triumph} total)" | ||||||
|  |  | ||||||
|  |             status = f"{Config.triumph_icon} {Config.triumph_message} {total}" | ||||||
|  |  | ||||||
|  |         elif self.method == Method.hit: | ||||||
|  |             if Args.score: | ||||||
|  |                 total = f"(Score: {self.get_score()})" | ||||||
|  |             else: | ||||||
|  |                 total = f"({self.hits} total)" | ||||||
|  |  | ||||||
|  |             status = f"{Config.hit_icon} {Config.hit_message} {total}" | ||||||
|  |  | ||||||
|  |         elif self.method == Method.think: | ||||||
|  |             status = f"Thinking about {status}" | ||||||
|  |  | ||||||
|  |         elif self.method == Method.travel: | ||||||
|  |             status = f"Traveling to {status}" | ||||||
|  |  | ||||||
|  |         return status | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Ants: | ||||||
|  |     ants: ClassVar[list[Ant]] = [] | ||||||
|  |     top: ClassVar[Ant | None] = None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         Ants.get() | ||||||
|  |         Ants.check() | ||||||
|  |         Ants.get_top() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def check() -> None: | ||||||
|  |         if not Ants.ants: | ||||||
|  |             Ants.populate(Config.default_population) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def hatch(num: int = 1, ignore: list[str] | None = None) -> None: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         for _ in range(num): | ||||||
|  |             ant = Ant() | ||||||
|  |             ant.name = Ants.random_name(ignore) | ||||||
|  |             Ants.ants.append(ant) | ||||||
|  |             Game.update(ant) | ||||||
|  |  | ||||||
|  |         Ants.on_change() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def on_change() -> None: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         Ants.get_top() | ||||||
|  |         Game.info() | ||||||
|  |         Ants.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_ant(ignore: list[Ant] | None = None) -> Ant | None: | ||||||
|  |         if ignore: | ||||||
|  |             ants = [a for a in Ants.ants if a not in ignore] | ||||||
|  |         else: | ||||||
|  |             ants = Ants.ants | ||||||
|  |  | ||||||
|  |         return random.choice(ants) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_names() -> list[str]: | ||||||
|  |         return [ant.name for ant in Ants.ants] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def save() -> None: | ||||||
|  |         Storage.save_ants(Ants.ants) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_next() -> Ant | None: | ||||||
|  |         now = Utils.now() | ||||||
|  |         ages = [(now - ant.updated) for ant in Ants.ants] | ||||||
|  |  | ||||||
|  |         # Normalize ages to create weights | ||||||
|  |         total_age = sum(ages) | ||||||
|  |  | ||||||
|  |         if total_age == 0: | ||||||
|  |             weights = [1] * len(Ants.ants)  # If all ages are zero, use equal weights | ||||||
|  |         else: | ||||||
|  |             weights = [ | ||||||
|  |                 int((age / total_age) * 1000) for age in ages | ||||||
|  |             ]  # Scale and cast to int | ||||||
|  |  | ||||||
|  |         # Perform weighted random selection | ||||||
|  |         return random.choices(Ants.ants, weights=weights, k=1)[0] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_current() -> Ant | None: | ||||||
|  |         return max(Ants.ants, key=lambda ant: ant.updated) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_status(ant: Ant, status: str, method: str) -> None: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         status = status.strip() | ||||||
|  |         ant.status = status | ||||||
|  |         ant.method = method | ||||||
|  |         ant.updated = Utils.now() | ||||||
|  |  | ||||||
|  |         Game.update(ant) | ||||||
|  |         Ants.on_change() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get() -> None: | ||||||
|  |         if Args.clean: | ||||||
|  |             objs = [] | ||||||
|  |         else: | ||||||
|  |             objs = Storage.get_ants() | ||||||
|  |  | ||||||
|  |         for obj in objs: | ||||||
|  |             ant = Ant() | ||||||
|  |             ant.from_dict(obj) | ||||||
|  |             Ants.ants.append(ant) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def populate(num: int) -> None: | ||||||
|  |         Ants.clear() | ||||||
|  |         Ants.hatch(num) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_name(ignore: list[str] | None = None) -> str: | ||||||
|  |         names = Ants.get_names() | ||||||
|  |  | ||||||
|  |         if ignore: | ||||||
|  |             for name in ignore: | ||||||
|  |                 if name not in names: | ||||||
|  |                     names.append(name) | ||||||
|  |  | ||||||
|  |         return Utils.random_name(names) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_top() -> None: | ||||||
|  |         top: Ant | None = None | ||||||
|  |         top_score = 0 | ||||||
|  |  | ||||||
|  |         for ant in Ants.ants: | ||||||
|  |             score = ant.get_score() | ||||||
|  |  | ||||||
|  |             if (not top) or (score > top_score): | ||||||
|  |                 top = ant | ||||||
|  |                 top_score = score | ||||||
|  |             elif score == top_score: | ||||||
|  |                 if ant.created < top.created: | ||||||
|  |                     top = ant | ||||||
|  |  | ||||||
|  |         if not top: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         Ants.top = top | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def merge(ant_1: Ant | None = None) -> bool: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         def split(ant: Ant) -> list[str]: | ||||||
|  |             return re.split(r"[ -]", ant.name) | ||||||
|  |  | ||||||
|  |         def remove(words: list[str], ignore: list[str]) -> list[str]: | ||||||
|  |             return [word for word in words if word.lower() not in ignore] | ||||||
|  |  | ||||||
|  |         def fill(words: list[str]) -> list[str]: | ||||||
|  |             words = remove(words, ["of", "de", "da", "the"]) | ||||||
|  |  | ||||||
|  |             if len(words) < 2: | ||||||
|  |                 n = random.randint(1, 2) | ||||||
|  |  | ||||||
|  |                 if n == 1: | ||||||
|  |                     words = Utils.random_words(2 - len(words)) | ||||||
|  |                 else: | ||||||
|  |                     words = Utils.make_words(2 - len(words)) | ||||||
|  |  | ||||||
|  |                 words.extend(words) | ||||||
|  |  | ||||||
|  |             return [Utils.capitalize(word) for word in words] | ||||||
|  |  | ||||||
|  |         if not ant_1: | ||||||
|  |             ant_1 = Ants.random_ant() | ||||||
|  |  | ||||||
|  |         if not ant_1: | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         ant_2 = Ants.random_ant([ant_1]) | ||||||
|  |  | ||||||
|  |         if not ant_2: | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         words_1 = split(ant_1) | ||||||
|  |         words_2 = split(ant_2) | ||||||
|  |         words_1 = fill(words_1) | ||||||
|  |         words_2 = fill(words_2) | ||||||
|  |  | ||||||
|  |         name = "" | ||||||
|  |         names = Ants.get_names() | ||||||
|  |         combinations = list(itertools.product(words_1, words_2)) | ||||||
|  |         random.shuffle(combinations) | ||||||
|  |  | ||||||
|  |         for combo in combinations: | ||||||
|  |             possible = f"{combo[0]} {combo[1]}" | ||||||
|  |  | ||||||
|  |             if (possible == ant_1.name) or (possible == ant_2.name): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             if (possible in names) or (possible in Utils.names): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             name = possible | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |         if not name: | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         Ants.set_terminated(ant_1) | ||||||
|  |         Ants.set_terminated(ant_2) | ||||||
|  |  | ||||||
|  |         ant = Ant() | ||||||
|  |         ant.name = name | ||||||
|  |         ant.triumph = ant_1.triumph + ant_2.triumph | ||||||
|  |         ant.hits = ant_1.hits + ant_2.hits | ||||||
|  |  | ||||||
|  |         Ants.ants.append(ant) | ||||||
|  |         Game.update(ant) | ||||||
|  |         Ants.hatch(ignore=[ant_1.name, ant_2.name]) | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def clear() -> None: | ||||||
|  |         Ants.ants = [] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def terminate(ant: Ant) -> None: | ||||||
|  |         Ants.set_terminated(ant) | ||||||
|  |         Ants.hatch(ignore=[ant.name]) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_terminated(ant: Ant) -> None: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         ant.method = "terminated" | ||||||
|  |         Game.update(ant) | ||||||
|  |         Ants.ants.remove(ant) | ||||||
							
								
								
									
										216
									
								
								cromulant/args.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,216 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import argparse | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .utils import Utils | ||||||
|  | from .argspec import ArgSpec | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Args: | ||||||
|  |     names: Path | None = None | ||||||
|  |     ants: Path | None = None | ||||||
|  |     images: bool = True | ||||||
|  |     header: bool = True | ||||||
|  |     footer: bool = True | ||||||
|  |     intro: bool = True | ||||||
|  |     title: str = "" | ||||||
|  |     width: int = 0 | ||||||
|  |     height: int = 0 | ||||||
|  |     program: str = "" | ||||||
|  |     speed: str = "" | ||||||
|  |     clean: bool = False | ||||||
|  |     fast_seconds: int = 0 | ||||||
|  |     normal_minutes: float = 0.0 | ||||||
|  |     slow_minutes: float = 0.0 | ||||||
|  |     argdoc: bool = False | ||||||
|  |     score: bool = False | ||||||
|  |     mono: bool = False | ||||||
|  |     fade: bool = True | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         ArgSpec.prepare() | ||||||
|  |         ArgParser.prepare(Config.title, ArgSpec.arguments) | ||||||
|  |  | ||||||
|  |         for attr_name, attr_value in vars(Args).items(): | ||||||
|  |             ArgSpec.defaults[attr_name] = attr_value | ||||||
|  |  | ||||||
|  |         other_name = [ | ||||||
|  |             ("no_images", "images"), | ||||||
|  |             ("no_header", "header"), | ||||||
|  |             ("no_footer", "footer"), | ||||||
|  |             ("no_intro", "intro"), | ||||||
|  |             ("no_fade", "fade"), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for r_item in other_name: | ||||||
|  |             ArgParser.get_value(*r_item) | ||||||
|  |  | ||||||
|  |         normals = [ | ||||||
|  |             "title", | ||||||
|  |             "width", | ||||||
|  |             "height", | ||||||
|  |             "program", | ||||||
|  |             "speed", | ||||||
|  |             "clean", | ||||||
|  |             "fast_seconds", | ||||||
|  |             "normal_minutes", | ||||||
|  |             "slow_minutes", | ||||||
|  |             "argdoc", | ||||||
|  |             "score", | ||||||
|  |             "mono", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for n_item in normals: | ||||||
|  |             ArgParser.get_value(n_item) | ||||||
|  |  | ||||||
|  |         paths = [ | ||||||
|  |             "names", | ||||||
|  |             "ants", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for p_item in paths: | ||||||
|  |             ArgParser.get_value(p_item, path=True) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_argdoc() -> None: | ||||||
|  |         from .utils import Utils | ||||||
|  |         from .storage import Storage | ||||||
|  |  | ||||||
|  |         text = Args.argtext() | ||||||
|  |         Storage.save_arguments(text) | ||||||
|  |         Utils.print("Saved arguments document") | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def argtext(filter_text: str | None = None) -> str: | ||||||
|  |         sep = "\n\n---\n\n" | ||||||
|  |         text = "" | ||||||
|  |         filter_lower = "" | ||||||
|  |  | ||||||
|  |         if not filter_text: | ||||||
|  |             text = "# Arguments\n\n" | ||||||
|  |             text += "Here are all the available command line arguments:" | ||||||
|  |         else: | ||||||
|  |             filter_lower = filter_text.lower() | ||||||
|  |  | ||||||
|  |         for key in ArgSpec.arguments: | ||||||
|  |             if key == "string_arg": | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             arg = ArgSpec.arguments[key] | ||||||
|  |             info = arg.get("help", "") | ||||||
|  |  | ||||||
|  |             if filter_text: | ||||||
|  |                 if filter_lower not in key.lower(): | ||||||
|  |                     if filter_lower not in info.lower(): | ||||||
|  |                         continue | ||||||
|  |  | ||||||
|  |             text += sep | ||||||
|  |             name = key.replace("_", "-") | ||||||
|  |             text += f"### {name}" | ||||||
|  |  | ||||||
|  |             if info: | ||||||
|  |                 text += "\n\n" | ||||||
|  |                 text += info | ||||||
|  |  | ||||||
|  |             defvalue = ArgSpec.defaults.get(key) | ||||||
|  |  | ||||||
|  |             if defvalue is not None: | ||||||
|  |                 if isinstance(defvalue, str): | ||||||
|  |                     if defvalue == "": | ||||||
|  |                         defvalue = "[Empty string]" | ||||||
|  |                     elif defvalue.strip() == "": | ||||||
|  |                         spaces = defvalue.count(" ") | ||||||
|  |                         ds = Utils.singular_or_plural(spaces, "space", "spaces") | ||||||
|  |                         defvalue = f"[{spaces} {ds}]" | ||||||
|  |                     else: | ||||||
|  |                         defvalue = f'"{defvalue}"' | ||||||
|  |  | ||||||
|  |                 text += "\n\n" | ||||||
|  |                 text += f"Default: {defvalue}" | ||||||
|  |  | ||||||
|  |             choices = arg.get("choices", []) | ||||||
|  |  | ||||||
|  |             if choices: | ||||||
|  |                 text += "\n\n" | ||||||
|  |                 text += "Choices: " | ||||||
|  |  | ||||||
|  |                 choicestr = [ | ||||||
|  |                     f'"{choice}"' if isinstance(choice, str) else choice | ||||||
|  |                     for choice in choices | ||||||
|  |                 ] | ||||||
|  |  | ||||||
|  |                 text += ", ".join(choicestr) | ||||||
|  |  | ||||||
|  |             action = arg.get("action", "") | ||||||
|  |  | ||||||
|  |             if action: | ||||||
|  |                 text += "\n\n" | ||||||
|  |                 text += f"Action: {action}" | ||||||
|  |  | ||||||
|  |             argtype = arg.get("type", "") | ||||||
|  |  | ||||||
|  |             if argtype: | ||||||
|  |                 text += "\n\n" | ||||||
|  |                 text += f"Type: {argtype.__name__}" | ||||||
|  |  | ||||||
|  |         text += "\n" | ||||||
|  |         return text.lstrip() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ArgParser: | ||||||
|  |     parser: argparse.ArgumentParser | ||||||
|  |     args: argparse.Namespace | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare(title: str, argdefs: dict[str, Any]) -> None: | ||||||
|  |         parser = argparse.ArgumentParser(description=title) | ||||||
|  |         argdefs["string_arg"] = {"nargs": "*"} | ||||||
|  |  | ||||||
|  |         for key in argdefs: | ||||||
|  |             item = argdefs[key] | ||||||
|  |  | ||||||
|  |             if key == "string_arg": | ||||||
|  |                 name = key | ||||||
|  |             else: | ||||||
|  |                 name = ArgParser.under_to_dash(key) | ||||||
|  |                 name = f"--{name}" | ||||||
|  |  | ||||||
|  |             tail = {key: value for key, value in item.items() if value is not None} | ||||||
|  |             parser.add_argument(name, **tail) | ||||||
|  |  | ||||||
|  |         ArgParser.parser = parser | ||||||
|  |         ArgParser.args = parser.parse_args() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def string_arg() -> str: | ||||||
|  |         return " ".join(ArgParser.args.string_arg) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_value( | ||||||
|  |         attr: str, key: str | None = None, no_strip: bool = False, path: bool = False | ||||||
|  |     ) -> None: | ||||||
|  |         value = getattr(ArgParser.args, attr) | ||||||
|  |  | ||||||
|  |         if value is not None: | ||||||
|  |             if not no_strip: | ||||||
|  |                 if isinstance(value, str): | ||||||
|  |                     value = value.strip() | ||||||
|  |  | ||||||
|  |             obj = key if key else attr | ||||||
|  |  | ||||||
|  |             if path: | ||||||
|  |                 value = Path(value) | ||||||
|  |  | ||||||
|  |             ArgParser.set(obj, value) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set(attr: str, value: Any) -> None: | ||||||
|  |         setattr(Args, attr, value) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def under_to_dash(s: str) -> str: | ||||||
|  |         return s.replace("_", "-") | ||||||
							
								
								
									
										186
									
								
								cromulant/argspec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,186 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DuplicateArgumentError(Exception): | ||||||
|  |     def __init__(self, key: str) -> None: | ||||||
|  |         self.message = f"Duplicate argument: {key}" | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return self.message | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MissingInfoError(Exception): | ||||||
|  |     def __init__(self, key: str) -> None: | ||||||
|  |         self.message = f"Missing info for argument: {key}" | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return self.message | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DuplicateInfoError(Exception): | ||||||
|  |     def __init__(self, key: str) -> None: | ||||||
|  |         self.message = f"Duplicate info for argument: {key}" | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return self.message | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ArgSpec: | ||||||
|  |     vinfo: str | ||||||
|  |     defaults: dict[str, Any] | ||||||
|  |     arguments: dict[str, Any] | ||||||
|  |     infos: list[str] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         ArgSpec.vinfo = f"{Config.title} {Config.version}" | ||||||
|  |         ArgSpec.defaults = {} | ||||||
|  |         ArgSpec.arguments = {} | ||||||
|  |         ArgSpec.infos = [] | ||||||
|  |         ArgSpec.add_arguments() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_argument(key: str, info: str, **kwargs: Any) -> None: | ||||||
|  |         if key in ArgSpec.arguments: | ||||||
|  |             raise DuplicateArgumentError(key) | ||||||
|  |  | ||||||
|  |         if not info: | ||||||
|  |             raise MissingInfoError(key) | ||||||
|  |  | ||||||
|  |         if info in ArgSpec.infos: | ||||||
|  |             raise DuplicateInfoError(key) | ||||||
|  |  | ||||||
|  |         ArgSpec.arguments[key] = { | ||||||
|  |             "help": info, | ||||||
|  |             **kwargs, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ArgSpec.infos.append(info) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_arguments() -> None: | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "version", | ||||||
|  |             action="version", | ||||||
|  |             info="Check the version of the program", | ||||||
|  |             version=ArgSpec.vinfo, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "names", | ||||||
|  |             type=str, | ||||||
|  |             info="Path to a JSON file with a list of names. Use these instead of the default ones", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "ants", | ||||||
|  |             type=str, | ||||||
|  |             info="Path to a JSON file with ants data. Use this instead of the default one", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "no_images", | ||||||
|  |             action="store_false", | ||||||
|  |             info="Don't show the images on the left", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "no_header", | ||||||
|  |             action="store_false", | ||||||
|  |             info="Don't show the header controls", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "no_footer", | ||||||
|  |             action="store_false", | ||||||
|  |             info="Don't show the footer controls", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "no_intro", | ||||||
|  |             action="store_false", | ||||||
|  |             info="Don't show the intro message", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "title", | ||||||
|  |             type=str, | ||||||
|  |             info="Custom title for the window", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "width", | ||||||
|  |             type=int, | ||||||
|  |             info="The width of the window in pixels", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "height", | ||||||
|  |             type=int, | ||||||
|  |             info="The height of the window in pixels", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "program", | ||||||
|  |             type=str, | ||||||
|  |             info="The internal name of the program", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "speed", | ||||||
|  |             type=str, | ||||||
|  |             choices=["fast", "normal", "slow", "paused"], | ||||||
|  |             info="Use this update speed", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "clean", | ||||||
|  |             action="store_true", | ||||||
|  |             info="Start with clean ants data", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "fast_seconds", | ||||||
|  |             type=int, | ||||||
|  |             info="The number of seconds between fast updates", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "normal_minutes", | ||||||
|  |             type=float, | ||||||
|  |             info="The number of minutes between normal updates", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "slow_minutes", | ||||||
|  |             type=float, | ||||||
|  |             info="The number of minutes between slow updates", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "argdoc", | ||||||
|  |             action="store_true", | ||||||
|  |             info="Make the arguments document and exit", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "score", | ||||||
|  |             action="store_true", | ||||||
|  |             info="Show the score on triumph or hits instead of the total of each", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "mono", | ||||||
|  |             action="store_true", | ||||||
|  |             info="Use a monospace font", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ArgSpec.add_argument( | ||||||
|  |             "no_fade", | ||||||
|  |             action="store_false", | ||||||
|  |             info="Don't apply a fade-in effect on new updates", | ||||||
|  |         ) | ||||||
							
								
								
									
										
											BIN
										
									
								
								cromulant/audio/March of the Cyber Ants.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										105
									
								
								cromulant/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,105 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | import appdirs  # type: ignore | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Config: | ||||||
|  |     program: str | ||||||
|  |     title: str | ||||||
|  |     version: str | ||||||
|  |     width: int = 820 | ||||||
|  |     height: int = 900 | ||||||
|  |     here: Path | ||||||
|  |     ants_json: Path | ||||||
|  |     icon_path: Path | ||||||
|  |     status_image_path: Path | ||||||
|  |     hatched_image_path: Path | ||||||
|  |     top_image_path: Path | ||||||
|  |     terminated_image_path: Path | ||||||
|  |     names_json: Path | ||||||
|  |     background_color: str = "rgb(44, 44, 44)" | ||||||
|  |     text_color: str = "#ffffff" | ||||||
|  |     image_size: int = 80 | ||||||
|  |     space_1: int = 18 | ||||||
|  |     max_updates: int = 300 | ||||||
|  |     fast_seconds: int = 5 | ||||||
|  |     normal_minutes: float = 1 | ||||||
|  |     slow_minutes: float = 5 | ||||||
|  |     font_size: int = 20 | ||||||
|  |     info_separator: str = "  -  " | ||||||
|  |     font_path: Path | ||||||
|  |     emoji_font_path: Path | ||||||
|  |     mono_font_path: Path | ||||||
|  |     triumph_color: tuple[int, int, int] = (255, 255, 0) | ||||||
|  |     hit_color: tuple[int, int, int] = (255, 0, 77) | ||||||
|  |     triumph_icon: str = "😀" | ||||||
|  |     hit_icon: str = "🎃" | ||||||
|  |     triumph_message: str = "Scored a triumph" | ||||||
|  |     hit_message: str = "Took a hit" | ||||||
|  |     song_path: Path | ||||||
|  |     logo_path: Path | ||||||
|  |     alt_background_color: str = "rgb(33, 33, 33)" | ||||||
|  |     alt_text_color: str = "white" | ||||||
|  |     alt_hover_background_color: str = "rgb(51, 51, 51)" | ||||||
|  |     alt_hover_text_color: str = "white" | ||||||
|  |     alt_border_color: str = "rgb(88, 88, 88)" | ||||||
|  |     message_box_button_hover_background_color: str = "rgb(66, 66, 66)" | ||||||
|  |     message_box_button_hover_text_color: str = "white" | ||||||
|  |     scrollbar_handle_color: str = "rgb(69, 69, 69)" | ||||||
|  |     input_background_color: str = "rgb(111, 111, 111)" | ||||||
|  |     input_text_color: str = "black" | ||||||
|  |     input_border_color: str = "rgb(120, 120, 120)" | ||||||
|  |     input_caret_color: str = "rgb(18, 18, 18)" | ||||||
|  |     settings_json: Path | ||||||
|  |     countries_json: Path | ||||||
|  |     filter_debouncer_delay: int = 200 | ||||||
|  |     default_population: int = 100 | ||||||
|  |     merge_goal: int = 10 | ||||||
|  |     manifest_path: Path | ||||||
|  |     manifest: dict[str, str] | ||||||
|  |     icon_on: str = "✅" | ||||||
|  |     icon_off: str = "❌" | ||||||
|  |     ant: str = "🐜" | ||||||
|  |     arguments_path: Path | ||||||
|  |     fade_duration: int = 500 | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         from .storage import Storage | ||||||
|  |  | ||||||
|  |         Config.here = Path(__file__).parent | ||||||
|  |         Config.manifest_path = Config.here / "manifest.json" | ||||||
|  |         Config.manifest = Storage.get_manifest() | ||||||
|  |         Config.title = Config.manifest["title"] | ||||||
|  |         Config.program = Config.manifest["program"] | ||||||
|  |         Config.version = Config.manifest["version"] | ||||||
|  |  | ||||||
|  |         Config.ants_json = Path(appdirs.user_data_dir()) / Config.program / "ants.json" | ||||||
|  |  | ||||||
|  |         if not Config.ants_json.exists(): | ||||||
|  |             Config.ants_json.parent.mkdir(parents=True, exist_ok=True) | ||||||
|  |             Config.ants_json.write_text("[]") | ||||||
|  |  | ||||||
|  |         Config.settings_json = ( | ||||||
|  |             Path(appdirs.user_config_dir()) / Config.program / "settings.json" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if not Config.settings_json.exists(): | ||||||
|  |             Config.settings_json.parent.mkdir(parents=True, exist_ok=True) | ||||||
|  |             Config.settings_json.write_text("{}") | ||||||
|  |  | ||||||
|  |         Config.names_json = Config.here / "data" / "names.json" | ||||||
|  |         Config.countries_json = Config.here / "data" / "countries.json" | ||||||
|  |         Config.icon_path = Config.here / "img" / "icon.jpg" | ||||||
|  |         Config.status_image_path = Config.here / "img" / "status.jpg" | ||||||
|  |         Config.hatched_image_path = Config.here / "img" / "hatched.jpg" | ||||||
|  |         Config.top_image_path = Config.here / "img" / "top.jpg" | ||||||
|  |         Config.terminated_image_path = Config.here / "img" / "terminated.jpg" | ||||||
|  |         Config.font_path = Config.here / "fonts" / "NotoSans-Regular.ttf" | ||||||
|  |         Config.emoji_font_path = Config.here / "fonts" / "NotoEmoji-Regular.ttf" | ||||||
|  |         Config.mono_font_path = Config.here / "fonts" / "NotoSansMono-Regular.ttf" | ||||||
|  |         Config.song_path = Config.here / "audio" / "March of the Cyber Ants.mp3" | ||||||
|  |         Config.logo_path = Config.here / "img" / "logo_3.jpg" | ||||||
|  |         Config.arguments_path = Config.here / ".." / "arguments.md" | ||||||
							
								
								
									
										250
									
								
								cromulant/data/countries.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,250 @@ | |||||||
|  | [ | ||||||
|  |     "Afghanistan", | ||||||
|  |     "Albania", | ||||||
|  |     "Algeria", | ||||||
|  |     "American Samoa", | ||||||
|  |     "Andorra", | ||||||
|  |     "Angola", | ||||||
|  |     "Anguilla", | ||||||
|  |     "Antarctica", | ||||||
|  |     "Antigua and Barbuda", | ||||||
|  |     "Argentina", | ||||||
|  |     "Armenia", | ||||||
|  |     "Aruba", | ||||||
|  |     "Australia", | ||||||
|  |     "Austria", | ||||||
|  |     "Azerbaijan", | ||||||
|  |     "Bahamas", | ||||||
|  |     "Bahrain", | ||||||
|  |     "Bangladesh", | ||||||
|  |     "Barbados", | ||||||
|  |     "Belarus", | ||||||
|  |     "Belgium", | ||||||
|  |     "Belize", | ||||||
|  |     "Benin", | ||||||
|  |     "Bermuda", | ||||||
|  |     "Bhutan", | ||||||
|  |     "Bolivia", | ||||||
|  |     "Bosnia and Herzegovina", | ||||||
|  |     "Botswana", | ||||||
|  |     "Bouvet Island", | ||||||
|  |     "Brazil", | ||||||
|  |     "British Indian Ocean Territory", | ||||||
|  |     "Brunei", | ||||||
|  |     "Bulgaria", | ||||||
|  |     "Burkina Faso", | ||||||
|  |     "Burundi", | ||||||
|  |     "Cambodia", | ||||||
|  |     "Cameroon", | ||||||
|  |     "Canada", | ||||||
|  |     "Cape Verde", | ||||||
|  |     "Cayman Islands", | ||||||
|  |     "Central African Republic", | ||||||
|  |     "Chad", | ||||||
|  |     "Chile", | ||||||
|  |     "China", | ||||||
|  |     "Christmas Island", | ||||||
|  |     "Cocos (Keeling) Islands", | ||||||
|  |     "Colombia", | ||||||
|  |     "Comoros", | ||||||
|  |     "Congo", | ||||||
|  |     "The Democratic Republic of Congo", | ||||||
|  |     "Cook Islands", | ||||||
|  |     "Costa Rica", | ||||||
|  |     "Ivory Coast", | ||||||
|  |     "Croatia", | ||||||
|  |     "Cuba", | ||||||
|  |     "Cyprus", | ||||||
|  |     "Czech Republic", | ||||||
|  |     "Denmark", | ||||||
|  |     "Djibouti", | ||||||
|  |     "Dominica", | ||||||
|  |     "Dominican Republic", | ||||||
|  |     "East Timor", | ||||||
|  |     "Ecuador", | ||||||
|  |     "Egypt", | ||||||
|  |     "England", | ||||||
|  |     "El Salvador", | ||||||
|  |     "Equatorial Guinea", | ||||||
|  |     "Eritrea", | ||||||
|  |     "Estonia", | ||||||
|  |     "Eswatini", | ||||||
|  |     "Ethiopia", | ||||||
|  |     "Falkland Islands", | ||||||
|  |     "Faroe Islands", | ||||||
|  |     "Fiji Islands", | ||||||
|  |     "Finland", | ||||||
|  |     "France", | ||||||
|  |     "French Guiana", | ||||||
|  |     "French Polynesia", | ||||||
|  |     "French Southern territories", | ||||||
|  |     "Gabon", | ||||||
|  |     "Gambia", | ||||||
|  |     "Georgia", | ||||||
|  |     "Germany", | ||||||
|  |     "Ghana", | ||||||
|  |     "Gibraltar", | ||||||
|  |     "Greece", | ||||||
|  |     "Greenland", | ||||||
|  |     "Grenada", | ||||||
|  |     "Guadeloupe", | ||||||
|  |     "Guam", | ||||||
|  |     "Guatemala", | ||||||
|  |     "Guernsey", | ||||||
|  |     "Guinea", | ||||||
|  |     "Guinea-Bissau", | ||||||
|  |     "Guyana", | ||||||
|  |     "Haiti", | ||||||
|  |     "Heard Island and McDonald Islands", | ||||||
|  |     "Holy See (Vatican City State)", | ||||||
|  |     "Honduras", | ||||||
|  |     "Hong Kong", | ||||||
|  |     "Hungary", | ||||||
|  |     "Iceland", | ||||||
|  |     "India", | ||||||
|  |     "Indonesia", | ||||||
|  |     "Iran", | ||||||
|  |     "Iraq", | ||||||
|  |     "Ireland", | ||||||
|  |     "Israel", | ||||||
|  |     "Isle of Man", | ||||||
|  |     "Italy", | ||||||
|  |     "Jamaica", | ||||||
|  |     "Japan", | ||||||
|  |     "Jersey", | ||||||
|  |     "Jordan", | ||||||
|  |     "Kazakhstan", | ||||||
|  |     "Kenya", | ||||||
|  |     "Kiribati", | ||||||
|  |     "Kuwait", | ||||||
|  |     "Kyrgyzstan", | ||||||
|  |     "Laos", | ||||||
|  |     "Latvia", | ||||||
|  |     "Lebanon", | ||||||
|  |     "Lesotho", | ||||||
|  |     "Liberia", | ||||||
|  |     "Libya", | ||||||
|  |     "Liechtenstein", | ||||||
|  |     "Lithuania", | ||||||
|  |     "Luxembourg", | ||||||
|  |     "Macao", | ||||||
|  |     "North Macedonia", | ||||||
|  |     "Madagascar", | ||||||
|  |     "Malawi", | ||||||
|  |     "Malaysia", | ||||||
|  |     "Maldives", | ||||||
|  |     "Mali", | ||||||
|  |     "Malta", | ||||||
|  |     "Marshall Islands", | ||||||
|  |     "Martinique", | ||||||
|  |     "Mauritania", | ||||||
|  |     "Mauritius", | ||||||
|  |     "Mayotte", | ||||||
|  |     "Mexico", | ||||||
|  |     "Micronesia, Federated States of", | ||||||
|  |     "Moldova", | ||||||
|  |     "Monaco", | ||||||
|  |     "Mongolia", | ||||||
|  |     "Montserrat", | ||||||
|  |     "Montenegro", | ||||||
|  |     "Morocco", | ||||||
|  |     "Mozambique", | ||||||
|  |     "Myanmar", | ||||||
|  |     "Namibia", | ||||||
|  |     "Nauru", | ||||||
|  |     "Nepal", | ||||||
|  |     "Netherlands", | ||||||
|  |     "Netherlands Antilles", | ||||||
|  |     "New Caledonia", | ||||||
|  |     "New Zealand", | ||||||
|  |     "Nicaragua", | ||||||
|  |     "Niger", | ||||||
|  |     "Nigeria", | ||||||
|  |     "Niue", | ||||||
|  |     "Norfolk Island", | ||||||
|  |     "North Korea", | ||||||
|  |     "Northern Ireland", | ||||||
|  |     "Northern Mariana Islands", | ||||||
|  |     "Norway", | ||||||
|  |     "Oman", | ||||||
|  |     "Pakistan", | ||||||
|  |     "Palau", | ||||||
|  |     "Palestine", | ||||||
|  |     "Panama", | ||||||
|  |     "Papua New Guinea", | ||||||
|  |     "Paraguay", | ||||||
|  |     "Peru", | ||||||
|  |     "Philippines", | ||||||
|  |     "Pitcairn", | ||||||
|  |     "Poland", | ||||||
|  |     "Portugal", | ||||||
|  |     "Puerto Rico", | ||||||
|  |     "Qatar", | ||||||
|  |     "Reunion", | ||||||
|  |     "Romania", | ||||||
|  |     "Russia", | ||||||
|  |     "Rwanda", | ||||||
|  |     "Saint Helena", | ||||||
|  |     "Saint Kitts and Nevis", | ||||||
|  |     "Saint Lucia", | ||||||
|  |     "Saint Pierre and Miquelon", | ||||||
|  |     "Saint Vincent and the Grenadines", | ||||||
|  |     "Samoa", | ||||||
|  |     "San Marino", | ||||||
|  |     "Sao Tome and Principe", | ||||||
|  |     "Saudi Arabia", | ||||||
|  |     "Scotland", | ||||||
|  |     "Senegal", | ||||||
|  |     "Serbia", | ||||||
|  |     "Seychelles", | ||||||
|  |     "Sierra Leone", | ||||||
|  |     "Singapore", | ||||||
|  |     "Slovakia", | ||||||
|  |     "Slovenia", | ||||||
|  |     "Solomon Islands", | ||||||
|  |     "Somalia", | ||||||
|  |     "South Africa", | ||||||
|  |     "South Georgia and the South Sandwich Islands", | ||||||
|  |     "South Korea", | ||||||
|  |     "South Sudan", | ||||||
|  |     "Spain", | ||||||
|  |     "Sri Lanka", | ||||||
|  |     "Sudan", | ||||||
|  |     "Suriname", | ||||||
|  |     "Svalbard and Jan Mayen", | ||||||
|  |     "Sweden", | ||||||
|  |     "Switzerland", | ||||||
|  |     "Syria", | ||||||
|  |     "Tajikistan", | ||||||
|  |     "Tanzania", | ||||||
|  |     "Thailand", | ||||||
|  |     "Timor-Leste", | ||||||
|  |     "Togo", | ||||||
|  |     "Tokelau", | ||||||
|  |     "Tonga", | ||||||
|  |     "Trinidad and Tobago", | ||||||
|  |     "Tunisia", | ||||||
|  |     "Turkey", | ||||||
|  |     "Turkmenistan", | ||||||
|  |     "Turks and Caicos Islands", | ||||||
|  |     "Tuvalu", | ||||||
|  |     "Uganda", | ||||||
|  |     "Ukraine", | ||||||
|  |     "United Arab Emirates", | ||||||
|  |     "United Kingdom", | ||||||
|  |     "United States", | ||||||
|  |     "United States Minor Outlying Islands", | ||||||
|  |     "Uruguay", | ||||||
|  |     "Uzbekistan", | ||||||
|  |     "Vanuatu", | ||||||
|  |     "Venezuela", | ||||||
|  |     "Vietnam", | ||||||
|  |     "Virgin Islands, British", | ||||||
|  |     "Virgin Islands, U.S.", | ||||||
|  |     "Wales", | ||||||
|  |     "Wallis and Futuna", | ||||||
|  |     "Western Sahara", | ||||||
|  |     "Yemen", | ||||||
|  |     "Zambia", | ||||||
|  |     "Zimbabwe" | ||||||
|  | ] | ||||||
							
								
								
									
										1002
									
								
								cromulant/data/names.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										99
									
								
								cromulant/filter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,99 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from PySide6.QtWidgets import QWidget  # type: ignore | ||||||
|  | from PySide6.QtGui import QKeyEvent  # type: ignore | ||||||
|  | from PySide6.QtCore import QTimer  # type: ignore | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .window import Window | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Filter: | ||||||
|  |     debouncer: QTimer | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         Filter.debouncer = QTimer() | ||||||
|  |         Filter.debouncer.setSingleShot(True) | ||||||
|  |         Filter.debouncer.setInterval(Config.filter_debouncer_delay) | ||||||
|  |         Filter.debouncer.timeout.connect(Filter.do_filter) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_value() -> str: | ||||||
|  |         return str(Window.filter.text()).lower().strip() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_value(value: str) -> None: | ||||||
|  |         Window.filter.setText(value) | ||||||
|  |         Filter.do_filter() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def clear() -> None: | ||||||
|  |         Window.filter.clear() | ||||||
|  |         Filter.do_filter() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def filter(event: QKeyEvent | None = None) -> None: | ||||||
|  |         Filter.debouncer.stop() | ||||||
|  |         Filter.debouncer.start() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def do_filter() -> None: | ||||||
|  |         Filter.debouncer.stop() | ||||||
|  |         value = Filter.get_value() | ||||||
|  |  | ||||||
|  |         for i in range(Window.view.count()): | ||||||
|  |             item = Window.view.itemAt(i) | ||||||
|  |             text = Filter.get_text(item) | ||||||
|  |             hide = True | ||||||
|  |  | ||||||
|  |             for txt in text: | ||||||
|  |                 if value in txt: | ||||||
|  |                     hide = False | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |             if hide: | ||||||
|  |                 item.widget().hide() | ||||||
|  |             else: | ||||||
|  |                 item.widget().show() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_text(item: QWidget) -> list[str]: | ||||||
|  |         text = [] | ||||||
|  |         layout = item.widget().layout() | ||||||
|  |  | ||||||
|  |         for i in range(layout.count()): | ||||||
|  |             widget = layout.itemAt(i).widget() | ||||||
|  |  | ||||||
|  |             if not widget: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             name = widget.objectName() | ||||||
|  |  | ||||||
|  |             if name != "view_right": | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             layout2 = widget.layout() | ||||||
|  |  | ||||||
|  |             for j in range(layout2.count()): | ||||||
|  |                 wid = layout2.itemAt(j).widget() | ||||||
|  |  | ||||||
|  |                 if not wid: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 name = wid.objectName() | ||||||
|  |  | ||||||
|  |                 if not name: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 if (name == "view_title") or (name == "view_message"): | ||||||
|  |                     text.append(wid.text().lower()) | ||||||
|  |  | ||||||
|  |         return text | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def check() -> None: | ||||||
|  |         value = Filter.get_value() | ||||||
|  |  | ||||||
|  |         if value: | ||||||
|  |             Filter.filter() | ||||||
							
								
								
									
										
											BIN
										
									
								
								cromulant/fonts/NotoEmoji-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								cromulant/fonts/NotoSans-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								cromulant/fonts/NotoSansMono-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										605
									
								
								cromulant/game.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,605 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import random | ||||||
|  | from typing import Any, ClassVar | ||||||
|  |  | ||||||
|  | from PySide6.QtWidgets import QHBoxLayout  # type: ignore | ||||||
|  | from PySide6.QtWidgets import QVBoxLayout | ||||||
|  | from PySide6.QtWidgets import QLabel | ||||||
|  | from PySide6.QtWidgets import QWidget | ||||||
|  | from PySide6.QtWidgets import QFrame | ||||||
|  | from PySide6.QtWidgets import QMenu | ||||||
|  | from PySide6.QtWidgets import QDialog | ||||||
|  | from PySide6.QtWidgets import QGraphicsOpacityEffect | ||||||
|  | from PySide6.QtGui import QCursor  # type: ignore | ||||||
|  | from PySide6.QtGui import QMouseEvent | ||||||
|  | from PySide6.QtGui import QPixmap | ||||||
|  | from PySide6.QtGui import QAction | ||||||
|  | from PySide6.QtCore import QPropertyAnimation  # type: ignore | ||||||
|  | from PySide6.QtCore import QEasingCurve | ||||||
|  | from PySide6.QtCore import QSize | ||||||
|  | from PySide6.QtCore import QTimer | ||||||
|  | from PySide6.QtCore import Qt | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .args import Args | ||||||
|  | from .utils import Utils | ||||||
|  | from .ants import Ant | ||||||
|  | from .ants import Ants | ||||||
|  | from .window import Window | ||||||
|  | from .window import RestartDialog | ||||||
|  | from .settings import Settings | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Method: | ||||||
|  |     merge = "merge" | ||||||
|  |     triumph = "triumph" | ||||||
|  |     hit = "hit" | ||||||
|  |     travel = "travel" | ||||||
|  |     think = "think" | ||||||
|  |     words = "words" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Opt: | ||||||
|  |     value = 0 | ||||||
|  |  | ||||||
|  |     def __init__(self, weight: int, method: str) -> None: | ||||||
|  |         self.value = Opt.value | ||||||
|  |         self.weight = weight | ||||||
|  |         self.method = method | ||||||
|  |  | ||||||
|  |         Opt.value += 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Opts: | ||||||
|  |     merge = Opt(1, Method.merge) | ||||||
|  |     triumph = Opt(2, Method.triumph) | ||||||
|  |     hit = Opt(2, Method.hit) | ||||||
|  |     travel = Opt(2, Method.travel) | ||||||
|  |     think = Opt(2, Method.think) | ||||||
|  |     words = Opt(4, Method.words) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def opts_score() -> list[Opt]: | ||||||
|  |         return [Opts.triumph, Opts.hit] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def opts_travel() -> list[Opt]: | ||||||
|  |         return [Opts.travel] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def opts_think() -> list[Opt]: | ||||||
|  |         return [Opts.think] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def opts_words() -> list[Opt]: | ||||||
|  |         return [Opts.words] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Game: | ||||||
|  |     timer: QTimer | ||||||
|  |     playing_song: bool = False | ||||||
|  |     merge_charge: int = 0 | ||||||
|  |     speed: str = "paused" | ||||||
|  |     animations: ClassVar[list[QPropertyAnimation]] = [] | ||||||
|  |     started: bool = False | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         Game.timer = QTimer() | ||||||
|  |         Game.timer.timeout.connect(Game.get_status) | ||||||
|  |  | ||||||
|  |         Game.fill() | ||||||
|  |         Game.info() | ||||||
|  |  | ||||||
|  |         if Args.intro: | ||||||
|  |             Game.intro() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def update(ant: Ant) -> None: | ||||||
|  |         root = QWidget() | ||||||
|  |         container = QHBoxLayout() | ||||||
|  |         root.setContentsMargins(0, 0, 0, 0) | ||||||
|  |         container.setContentsMargins(0, 0, 0, 0) | ||||||
|  |  | ||||||
|  |         if Args.images: | ||||||
|  |             image_label = Game.get_image(ant) | ||||||
|  |             container.addWidget(image_label) | ||||||
|  |  | ||||||
|  |         right_container = Game.make_right_container(ant) | ||||||
|  |         container.addWidget(right_container) | ||||||
|  |  | ||||||
|  |         container.addSpacing(Config.space_1) | ||||||
|  |         root.setLayout(container) | ||||||
|  |         Game.add_item(root) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def message(text: str) -> None: | ||||||
|  |         root = QWidget() | ||||||
|  |         root.setContentsMargins(0, 10, 0, 10) | ||||||
|  |  | ||||||
|  |         container = QHBoxLayout() | ||||||
|  |         container.setAlignment(Qt.AlignCenter) | ||||||
|  |  | ||||||
|  |         left_line = QFrame() | ||||||
|  |         left_line.setFrameShape(QFrame.HLine) | ||||||
|  |         left_line.setFrameShadow(QFrame.Sunken) | ||||||
|  |         left_line.setObjectName("horizontal_line") | ||||||
|  |         left_line.setFixedHeight(2) | ||||||
|  |         Window.expand_2(left_line) | ||||||
|  |  | ||||||
|  |         label = QLabel(text) | ||||||
|  |         label.setTextInteractionFlags(Qt.TextSelectableByMouse) | ||||||
|  |  | ||||||
|  |         right_line = QFrame() | ||||||
|  |         right_line.setFrameShape(QFrame.HLine) | ||||||
|  |         right_line.setFrameShadow(QFrame.Sunken) | ||||||
|  |         right_line.setObjectName("horizontal_line") | ||||||
|  |         right_line.setFixedHeight(2) | ||||||
|  |         Window.expand_2(right_line) | ||||||
|  |  | ||||||
|  |         container.addWidget(left_line) | ||||||
|  |         container.addWidget(label) | ||||||
|  |         container.addWidget(right_line) | ||||||
|  |  | ||||||
|  |         container.setSpacing(Config.space_1 * 2) | ||||||
|  |         root.setLayout(container) | ||||||
|  |         Game.add_item(root) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_item(item: QWidget) -> None: | ||||||
|  |         from .filter import Filter | ||||||
|  |  | ||||||
|  |         animation: QPropertyAnimation | None = None | ||||||
|  |  | ||||||
|  |         if Game.started and Args.fade: | ||||||
|  |             animation = Game.add_fade(item) | ||||||
|  |  | ||||||
|  |         Window.view.insertWidget(0, item) | ||||||
|  |  | ||||||
|  |         if animation: | ||||||
|  |             animation.start() | ||||||
|  |  | ||||||
|  |         while Window.view.count() > Config.max_updates: | ||||||
|  |             item = Window.view.takeAt(Window.view.count() - 1) | ||||||
|  |  | ||||||
|  |             if item.widget(): | ||||||
|  |                 item.widget().deleteLater() | ||||||
|  |             elif item.layout(): | ||||||
|  |                 Window.delete_layout(item.layout()) | ||||||
|  |  | ||||||
|  |         Filter.check() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_right_container(ant: Ant) -> QWidget: | ||||||
|  |         if ant.method == "hatched": | ||||||
|  |             title = "Hatched" | ||||||
|  |             message = f"{ant.name} is born" | ||||||
|  |         elif ant.method == "terminated": | ||||||
|  |             title = "Terminated" | ||||||
|  |             message = f"{ant.name} is gone" | ||||||
|  |         else: | ||||||
|  |             title = ant.name | ||||||
|  |             message = ant.get_status() | ||||||
|  |  | ||||||
|  |         root = QWidget() | ||||||
|  |         root.setObjectName("view_right") | ||||||
|  |         container = QVBoxLayout() | ||||||
|  |         container.setAlignment(Qt.AlignTop) | ||||||
|  |  | ||||||
|  |         title_label = QLabel(title) | ||||||
|  |         title_label.setTextInteractionFlags(Qt.TextSelectableByMouse) | ||||||
|  |         title_label.setStyleSheet("font-weight: bold;") | ||||||
|  |         title_label.setWordWrap(True) | ||||||
|  |         title_label.setObjectName("view_title") | ||||||
|  |         Window.expand(title_label) | ||||||
|  |  | ||||||
|  |         message_label = QLabel(message) | ||||||
|  |         message_label.setTextInteractionFlags(Qt.TextSelectableByMouse) | ||||||
|  |         message_label.setWordWrap(True) | ||||||
|  |         message_label.setObjectName("view_message") | ||||||
|  |         Window.expand(message_label) | ||||||
|  |  | ||||||
|  |         container.addWidget(title_label) | ||||||
|  |         container.addWidget(message_label) | ||||||
|  |         root.setLayout(container) | ||||||
|  |  | ||||||
|  |         return root | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_image(ant: Ant) -> QLabel: | ||||||
|  |         if ant.method == "hatched": | ||||||
|  |             path = Config.hatched_image_path | ||||||
|  |         elif ant.method == "terminated": | ||||||
|  |             path = Config.terminated_image_path | ||||||
|  |         elif ant == Ants.top: | ||||||
|  |             path = Config.top_image_path | ||||||
|  |         else: | ||||||
|  |             path = Config.status_image_path | ||||||
|  |  | ||||||
|  |         if ant.method == "triumph": | ||||||
|  |             color = Config.triumph_color | ||||||
|  |         elif ant.method == "hit": | ||||||
|  |             color = Config.hit_color | ||||||
|  |         else: | ||||||
|  |             color = None | ||||||
|  |  | ||||||
|  |         tooltip = ant.tooltip() | ||||||
|  |         image_label = QLabel() | ||||||
|  |         image_label.setObjectName("view_image") | ||||||
|  |         pixmap = QPixmap(str(path)) | ||||||
|  |  | ||||||
|  |         scaled_pixmap = pixmap.scaled( | ||||||
|  |             Config.image_size, | ||||||
|  |             pixmap.height(), | ||||||
|  |             Qt.KeepAspectRatio, | ||||||
|  |             Qt.SmoothTransformation, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         image_label.setPixmap(scaled_pixmap) | ||||||
|  |         adjusted_size = scaled_pixmap.size() + QSize(4, 4) | ||||||
|  |         image_label.setFixedSize(adjusted_size) | ||||||
|  |  | ||||||
|  |         if color: | ||||||
|  |             rgb = Utils.get_rgb(color) | ||||||
|  |  | ||||||
|  |             style = f""" | ||||||
|  |             QLabel#view_image {{ | ||||||
|  |                 border: 2px solid {rgb}; | ||||||
|  |             }} | ||||||
|  |             """ | ||||||
|  |  | ||||||
|  |             image_label.setStyleSheet(style) | ||||||
|  |  | ||||||
|  |         if tooltip: | ||||||
|  |             image_label.setToolTip(tooltip) | ||||||
|  |  | ||||||
|  |         image_label.mousePressEvent = lambda event: Game.image_action(event, ant) | ||||||
|  |         return image_label | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_status() -> None: | ||||||
|  |         ant = Ants.get_next() | ||||||
|  |  | ||||||
|  |         if not ant: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         opts: list[Opt] = [] | ||||||
|  |  | ||||||
|  |         if Settings.score_enabled: | ||||||
|  |             opts.extend(Opts.opts_score()) | ||||||
|  |  | ||||||
|  |         if Settings.travel_enabled: | ||||||
|  |             opts.extend(Opts.opts_travel()) | ||||||
|  |  | ||||||
|  |         if Settings.think_enabled: | ||||||
|  |             opts.extend(Opts.opts_think()) | ||||||
|  |  | ||||||
|  |         if Settings.words_enabled: | ||||||
|  |             opts.extend(Opts.opts_words()) | ||||||
|  |  | ||||||
|  |         if not opts: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         values = [opt.value for opt in opts] | ||||||
|  |         weights = [opt.weight for opt in opts] | ||||||
|  |  | ||||||
|  |         if Game.merge_charge < Config.merge_goal: | ||||||
|  |             Game.merge_charge += 1 | ||||||
|  |  | ||||||
|  |         if Settings.merge: | ||||||
|  |             if Game.merge_charge >= Config.merge_goal: | ||||||
|  |                 opt = Opts.merge | ||||||
|  |                 values.insert(0, opt.value) | ||||||
|  |                 weights.insert(0, opt.weight) | ||||||
|  |  | ||||||
|  |         value = random.choices(values, weights=weights, k=1)[0] | ||||||
|  |  | ||||||
|  |         if value == Opts.merge.value: | ||||||
|  |             if Ants.merge(): | ||||||
|  |                 Game.merge_charge = 0 | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             value = Opts.words.value | ||||||
|  |  | ||||||
|  |         status = "" | ||||||
|  |         method = "" | ||||||
|  |  | ||||||
|  |         if value == Opts.triumph.value: | ||||||
|  |             ant.triumph += 1 | ||||||
|  |             method = Opts.triumph.method | ||||||
|  |  | ||||||
|  |         elif value == Opts.hit.value: | ||||||
|  |             ant.hits += 1 | ||||||
|  |             method = Opts.hit.method | ||||||
|  |  | ||||||
|  |         elif value == Opts.travel.value: | ||||||
|  |             status = Utils.random_country([]) | ||||||
|  |             method = Opts.travel.method | ||||||
|  |  | ||||||
|  |         elif value == Opts.think.value: | ||||||
|  |             method = Opts.think.method | ||||||
|  |             n = random.choices([1, 2, 3], weights=[1, 2, 2])[0] | ||||||
|  |  | ||||||
|  |             if n == 1: | ||||||
|  |                 status = Utils.random_name([], Ants.get_names()) | ||||||
|  |             elif n == 2: | ||||||
|  |                 status = Utils.random_emoji(3) | ||||||
|  |             elif n == 3: | ||||||
|  |                 status = Utils.random_word(noun=True, adj=False) | ||||||
|  |  | ||||||
|  |         elif value == Opts.words.value: | ||||||
|  |             method = Opts.words.method | ||||||
|  |             n = random.randint(1, 4) | ||||||
|  |  | ||||||
|  |             if n == 1: | ||||||
|  |                 status = Utils.words_1() | ||||||
|  |             elif n == 2: | ||||||
|  |                 status = Utils.words_2() | ||||||
|  |             elif n == 3: | ||||||
|  |                 status = Utils.words_3() | ||||||
|  |             elif n == 4: | ||||||
|  |                 status = Utils.words_4() | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             status = "???" | ||||||
|  |             method = "unknown" | ||||||
|  |  | ||||||
|  |         Ants.set_status(ant, status, method) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def fill() -> None: | ||||||
|  |         if not len(Ants.ants): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         ants = sorted(Ants.ants, key=lambda ant: ant.updated) | ||||||
|  |  | ||||||
|  |         for ant in ants: | ||||||
|  |             Game.update(ant) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def start_loop() -> None: | ||||||
|  |         Game.timer.stop() | ||||||
|  |         speed = Settings.speed | ||||||
|  |  | ||||||
|  |         if speed == "fast": | ||||||
|  |             minutes = (Args.fast_seconds or Config.fast_seconds) / 60 | ||||||
|  |         elif speed == "normal": | ||||||
|  |             minutes = Args.normal_minutes or Config.normal_minutes | ||||||
|  |         elif speed == "slow": | ||||||
|  |             minutes = Args.slow_minutes or Config.slow_minutes | ||||||
|  |         else: | ||||||
|  |             Game.speed = "paused" | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         Game.speed = speed | ||||||
|  |         msecs = minutes * 60 * 1000 | ||||||
|  |  | ||||||
|  |         if msecs < 1000: | ||||||
|  |             msecs = 1000 | ||||||
|  |  | ||||||
|  |         Game.timer.setInterval(msecs) | ||||||
|  |         Game.timer.start() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def update_speed() -> None: | ||||||
|  |         speed = Window.speed.currentText().lower() | ||||||
|  |  | ||||||
|  |         if speed == Settings.speed: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         Settings.set_speed(speed) | ||||||
|  |         Game.start_loop() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def info() -> None: | ||||||
|  |         text = [] | ||||||
|  |  | ||||||
|  |         # Non-breaking space | ||||||
|  |         nb = "\u00a0" | ||||||
|  |  | ||||||
|  |         if not len(Ants.ants): | ||||||
|  |             text.append("Hatch some ants") | ||||||
|  |         else: | ||||||
|  |             text.append(f"Ants:{nb}{len(Ants.ants)}") | ||||||
|  |             top = Ants.top | ||||||
|  |  | ||||||
|  |             if top: | ||||||
|  |                 score = top.get_score() | ||||||
|  |                 text.append(f"Top:{nb}{top.name} ({score})") | ||||||
|  |  | ||||||
|  |         Window.info.setText(Config.info_separator.join(text)) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_song() -> None: | ||||||
|  |         if Game.playing_song: | ||||||
|  |             Window.stop_audio() | ||||||
|  |             Game.playing_song = False | ||||||
|  |         else: | ||||||
|  |             path = str(Config.song_path) | ||||||
|  |  | ||||||
|  |             def on_stop() -> None: | ||||||
|  |                 Game.playing_song = False | ||||||
|  |  | ||||||
|  |             Window.play_audio(path, on_stop) | ||||||
|  |             Game.playing_song = True | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def restart() -> None: | ||||||
|  |         sizes = ["25", "50", "100", "250"] | ||||||
|  |         defindex = 0 | ||||||
|  |  | ||||||
|  |         for i, opt in enumerate(sizes): | ||||||
|  |             if int(opt) == Config.default_population: | ||||||
|  |                 defindex = i | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         size_opts = [f"{opt} ants" for opt in sizes] | ||||||
|  |         dialog = RestartDialog(size_opts, defindex) | ||||||
|  |         data: dict[str, Any] | None = None | ||||||
|  |  | ||||||
|  |         if dialog.exec() == QDialog.Accepted: | ||||||
|  |             data = dialog.get_data() | ||||||
|  |  | ||||||
|  |         if not data: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         size = int(data["size"].split(" ")[0]) | ||||||
|  |  | ||||||
|  |         Game.started = False | ||||||
|  |         Game.timer.stop() | ||||||
|  |         Window.clear_view() | ||||||
|  |         Ants.populate(size) | ||||||
|  |         Window.to_top() | ||||||
|  |         Game.intro() | ||||||
|  |         Game.start_loop() | ||||||
|  |         Game.started = True | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def update_size() -> None: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def image_action(event: QMouseEvent, ant: Ant) -> None: | ||||||
|  |         def is_terminated() -> bool: | ||||||
|  |             return ant.method == "terminated" | ||||||
|  |  | ||||||
|  |         if event.button() == Qt.LeftButton: | ||||||
|  |             if is_terminated(): | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             Ants.terminate(ant) | ||||||
|  |             Game.start_loop() | ||||||
|  |         elif event.button() == Qt.MiddleButton: | ||||||
|  |             if is_terminated(): | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             Ants.merge(ant) | ||||||
|  |             Game.start_loop() | ||||||
|  |         else: | ||||||
|  |             Game.toggle_song() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def intro() -> None: | ||||||
|  |         title = Config.title | ||||||
|  |         version = Config.version | ||||||
|  |         Game.message(f"Welcome to {title} v{version}") | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def menu() -> None: | ||||||
|  |         menu = QMenu(Window.root.widget()) | ||||||
|  |  | ||||||
|  |         style = f""" | ||||||
|  |         QMenu::separator {{ | ||||||
|  |             background-color: {Config.alt_border_color}; | ||||||
|  |         }} | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         menu.setStyleSheet(style) | ||||||
|  |         menu.setObjectName("main_menu") | ||||||
|  |         update = QAction("Update") | ||||||
|  |         restart = QAction("Restart") | ||||||
|  |         enable_all = QAction("Enable All") | ||||||
|  |         disable_all = QAction("Disable All") | ||||||
|  |         about = QAction("About") | ||||||
|  |  | ||||||
|  |         def make(text: str, enabled: bool) -> QAction: | ||||||
|  |             if enabled: | ||||||
|  |                 icon = Config.icon_on | ||||||
|  |                 word = "On" | ||||||
|  |             else: | ||||||
|  |                 icon = Config.icon_off | ||||||
|  |                 word = "Off" | ||||||
|  |  | ||||||
|  |             return QAction(f"{icon} {text} {word}") | ||||||
|  |  | ||||||
|  |         merge = make("Merge", Settings.merge) | ||||||
|  |         score = make("Score", Settings.score_enabled) | ||||||
|  |         travel = make("Travel", Settings.travel_enabled) | ||||||
|  |         think = make("Think", Settings.think_enabled) | ||||||
|  |         words = make("Words", Settings.words_enabled) | ||||||
|  |  | ||||||
|  |         update.triggered.connect(Game.force_update) | ||||||
|  |         restart.triggered.connect(Game.restart) | ||||||
|  |         merge.triggered.connect(Settings.toggle_merge) | ||||||
|  |         score.triggered.connect(Settings.toggle_score_enabled) | ||||||
|  |         travel.triggered.connect(Settings.toggle_travel_enabled) | ||||||
|  |         think.triggered.connect(Settings.toggle_think_enabled) | ||||||
|  |         words.triggered.connect(Settings.toggle_words_enabled) | ||||||
|  |         enable_all.triggered.connect(Settings.enable_all) | ||||||
|  |         disable_all.triggered.connect(Settings.disable_all) | ||||||
|  |         about.triggered.connect(Game.about) | ||||||
|  |  | ||||||
|  |         menu.addAction(update) | ||||||
|  |         menu.addAction(restart) | ||||||
|  |         menu.addSeparator() | ||||||
|  |         menu.addAction(merge) | ||||||
|  |         menu.addAction(score) | ||||||
|  |         menu.addAction(travel) | ||||||
|  |         menu.addAction(think) | ||||||
|  |         menu.addAction(words) | ||||||
|  |         menu.addSeparator() | ||||||
|  |         menu.addAction(enable_all) | ||||||
|  |         menu.addAction(disable_all) | ||||||
|  |         menu.addSeparator() | ||||||
|  |         menu.addAction(about) | ||||||
|  |         menu.exec_(QCursor.pos()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def force_update() -> None: | ||||||
|  |         Game.get_status() | ||||||
|  |         Game.start_loop() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def about() -> None: | ||||||
|  |         lines = [ | ||||||
|  |             f"{Config.title} v{Config.version}    {Config.ant}", | ||||||
|  |             "Listen to the ants and watch them go.", | ||||||
|  |             "Just run it and leave it open on your screen.", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         Window.alert("\n\n".join(lines)) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def slowdown() -> None: | ||||||
|  |         if Game.speed == "slow": | ||||||
|  |             Game.change_speed("paused") | ||||||
|  |         else: | ||||||
|  |             Game.change_speed("slow") | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def change_speed(speed: str) -> None: | ||||||
|  |         Window.speed.setCurrentText(speed.capitalize()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def filter_top() -> None: | ||||||
|  |         from .filter import Filter | ||||||
|  |  | ||||||
|  |         value = Filter.get_value() | ||||||
|  |         ant = Ants.top | ||||||
|  |  | ||||||
|  |         if not ant: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if value == ant.name.lower(): | ||||||
|  |             Filter.clear() | ||||||
|  |         else: | ||||||
|  |             Filter.set_value(ant.name) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_fade(item: QWidget) -> QPropertyAnimation: | ||||||
|  |         opacity = QGraphicsOpacityEffect(item) | ||||||
|  |         item.setGraphicsEffect(opacity) | ||||||
|  |         animation = QPropertyAnimation(opacity, b"opacity") | ||||||
|  |         animation.setDuration(Config.fade_duration) | ||||||
|  |         animation.setStartValue(0) | ||||||
|  |         animation.setEndValue(1) | ||||||
|  |         animation.setEasingCurve(QEasingCurve.InOutQuad) | ||||||
|  |  | ||||||
|  |         def on_finish() -> None: | ||||||
|  |             item.setGraphicsEffect(None) | ||||||
|  |             Game.animations.remove(animation) | ||||||
|  |  | ||||||
|  |         animation.finished.connect(on_finish) | ||||||
|  |         Game.animations.append(animation) | ||||||
|  |         return animation | ||||||
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/change.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 306 KiB | 
							
								
								
									
										3
									
								
								cromulant/img/green.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  |  | ||||||
|  | convert $1 -channel G -evaluate add 16% +channel -quality 85 -strip $1_green | ||||||
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/hatched.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 292 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/icon.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 492 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/logo_1.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 165 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/logo_2.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 188 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/logo_3.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 153 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/logo_4.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 146 KiB | 
							
								
								
									
										3
									
								
								cromulant/img/red.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  |  | ||||||
|  | convert $1 -channel R -evaluate add 16% +channel -quality 85 -strip $1_red | ||||||
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/status.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 271 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/terminated.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 284 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cromulant/img/top.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 415 KiB | 
							
								
								
									
										56
									
								
								cromulant/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import fcntl | ||||||
|  | import tempfile | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .utils import Utils | ||||||
|  | from .ants import Ants | ||||||
|  | from .window import Window | ||||||
|  | from .game import Game | ||||||
|  | from .settings import Settings | ||||||
|  | from .filter import Filter | ||||||
|  | from .args import Args | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main() -> None: | ||||||
|  |     Config.prepare() | ||||||
|  |     Args.prepare() | ||||||
|  |  | ||||||
|  |     if Args.argdoc: | ||||||
|  |         Args.make_argdoc() | ||||||
|  |         sys.exit(0) | ||||||
|  |  | ||||||
|  |     program = Config.program | ||||||
|  |     title = Config.title | ||||||
|  |  | ||||||
|  |     pid = f"{program}.pid" | ||||||
|  |     pid_file = Path(tempfile.gettempdir(), pid) | ||||||
|  |     fp = pid_file.open("w", encoding="utf-8") | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) | ||||||
|  |     except OSError: | ||||||
|  |         Utils.print(f"{title} is already running.") | ||||||
|  |         sys.exit(0) | ||||||
|  |  | ||||||
|  |     # Create singleton | ||||||
|  |     fp.write(str(os.getpid())) | ||||||
|  |     fp.flush() | ||||||
|  |  | ||||||
|  |     Utils.prepare() | ||||||
|  |     Window.prepare() | ||||||
|  |     Ants.prepare() | ||||||
|  |     Settings.prepare() | ||||||
|  |     Filter.prepare() | ||||||
|  |     Game.prepare() | ||||||
|  |     Game.start_loop() | ||||||
|  |     Game.started = True | ||||||
|  |     Window.start() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										8
									
								
								cromulant/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |     "version": "4.2.0", | ||||||
|  |     "title": "Cromulant", | ||||||
|  |     "program": "cromulant", | ||||||
|  |     "author": "madprops", | ||||||
|  |     "repo": "github.com/madprops/cromulant", | ||||||
|  |     "description": "Toy game about ants" | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								cromulant/ruff.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | |||||||
|  | [lint] | ||||||
|  |  | ||||||
|  | select = [ | ||||||
|  |     "T", | ||||||
|  |     "Q", | ||||||
|  |     "W", | ||||||
|  |     "B", | ||||||
|  |     "N", | ||||||
|  |     "F", | ||||||
|  |     "FA", | ||||||
|  |     "RET", | ||||||
|  |     "PTH", | ||||||
|  |     "ERA", | ||||||
|  |     "PLW", | ||||||
|  |     "PERF", | ||||||
|  |     "RUF", | ||||||
|  |     "FLY", | ||||||
|  |     "PT", | ||||||
|  |     "PYI", | ||||||
|  |     "PIE", | ||||||
|  |     "ICN", | ||||||
|  |     "UP", | ||||||
|  |     "TRY", | ||||||
|  |     "C4", | ||||||
|  |     "E401", | ||||||
|  |     "E713", | ||||||
|  |     "E721", | ||||||
|  |     "S101", | ||||||
|  |     "S113", | ||||||
|  |     "SIM103", | ||||||
|  |     "SIM114", | ||||||
|  |     "SIM118", | ||||||
|  |     "SIM210", | ||||||
|  |     "PLR5501", | ||||||
|  |     "PLR1711", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | ignore = [ | ||||||
|  |     "W292", | ||||||
|  |     "N802", | ||||||
|  |     "N815", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | exclude = [ | ||||||
|  |     "pyperclip.py", | ||||||
|  |     "tests.py", | ||||||
|  | ] | ||||||
							
								
								
									
										101
									
								
								cromulant/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,101 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from .args import Args | ||||||
|  | from .window import Window | ||||||
|  | from .storage import Storage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Settings: | ||||||
|  |     speed: str | ||||||
|  |     mode: str | ||||||
|  |     merge: bool | ||||||
|  |  | ||||||
|  |     score_enabled: bool | ||||||
|  |     travel_enabled: bool | ||||||
|  |     think_enabled: bool | ||||||
|  |     words_enabled: bool | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         settings = Storage.get_settings() | ||||||
|  |         changed = False | ||||||
|  |  | ||||||
|  |         if Args.speed: | ||||||
|  |             Settings.speed = Args.speed | ||||||
|  |             changed = True | ||||||
|  |         else: | ||||||
|  |             Settings.speed = settings.get("speed", "normal") | ||||||
|  |  | ||||||
|  |         speed = Settings.speed.capitalize() | ||||||
|  |         Window.speed.setCurrentText(speed) | ||||||
|  |  | ||||||
|  |         Settings.score_enabled = settings.get("score_enabled", True) | ||||||
|  |         Settings.travel_enabled = settings.get("travel_enabled", True) | ||||||
|  |         Settings.think_enabled = settings.get("think_enabled", True) | ||||||
|  |         Settings.words_enabled = settings.get("words_enabled", True) | ||||||
|  |  | ||||||
|  |         Settings.merge = settings.get("merge", True) | ||||||
|  |  | ||||||
|  |         if changed: | ||||||
|  |             Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def save() -> None: | ||||||
|  |         settings = { | ||||||
|  |             "speed": Settings.speed, | ||||||
|  |             "merge": Settings.merge, | ||||||
|  |             "score_enabled": Settings.score_enabled, | ||||||
|  |             "travel_enabled": Settings.travel_enabled, | ||||||
|  |             "think_enabled": Settings.think_enabled, | ||||||
|  |             "words_enabled": Settings.words_enabled, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Storage.save_settings(settings) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_speed(speed: str) -> None: | ||||||
|  |         Settings.speed = speed | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_merge() -> None: | ||||||
|  |         Settings.merge = not Settings.merge | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_score_enabled() -> None: | ||||||
|  |         Settings.score_enabled = not Settings.score_enabled | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_travel_enabled() -> None: | ||||||
|  |         Settings.travel_enabled = not Settings.travel_enabled | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_think_enabled() -> None: | ||||||
|  |         Settings.think_enabled = not Settings.think_enabled | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_words_enabled() -> None: | ||||||
|  |         Settings.words_enabled = not Settings.words_enabled | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def enable_all() -> None: | ||||||
|  |         Settings.merge = True | ||||||
|  |         Settings.score_enabled = True | ||||||
|  |         Settings.travel_enabled = True | ||||||
|  |         Settings.think_enabled = True | ||||||
|  |         Settings.words_enabled = True | ||||||
|  |         Settings.save() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def disable_all() -> None: | ||||||
|  |         Settings.merge = False | ||||||
|  |         Settings.score_enabled = False | ||||||
|  |         Settings.travel_enabled = False | ||||||
|  |         Settings.think_enabled = False | ||||||
|  |         Settings.words_enabled = False | ||||||
|  |         Settings.save() | ||||||
							
								
								
									
										90
									
								
								cromulant/storage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | from typing import TYPE_CHECKING, Any | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from .ants import Ant | ||||||
|  |  | ||||||
|  | from .args import Args | ||||||
|  | from .utils import Utils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Storage: | ||||||
|  |     @staticmethod | ||||||
|  |     def get_names_path() -> Path: | ||||||
|  |         path = Config.names_json | ||||||
|  |  | ||||||
|  |         if Args.names: | ||||||
|  |             if Args.names.exists(): | ||||||
|  |                 path = Args.names | ||||||
|  |  | ||||||
|  |         return path | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_ants_path() -> Path: | ||||||
|  |         path = Config.ants_json | ||||||
|  |  | ||||||
|  |         if Args.ants: | ||||||
|  |             if Args.ants.exists(): | ||||||
|  |                 path = Args.ants | ||||||
|  |  | ||||||
|  |         return path | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_ants() -> Any: | ||||||
|  |         try: | ||||||
|  |             path = Storage.get_ants_path() | ||||||
|  |  | ||||||
|  |             with path.open() as file: | ||||||
|  |                 return json.load(file) | ||||||
|  |         except Exception as e: | ||||||
|  |             Utils.print(str(e)) | ||||||
|  |             return [] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def save_ants(ants: list[Ant]) -> None: | ||||||
|  |         objs = [ant.to_dict() for ant in ants] | ||||||
|  |         path = Storage.get_ants_path() | ||||||
|  |  | ||||||
|  |         with path.open("w") as file: | ||||||
|  |             json.dump(objs, file) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_names() -> Any: | ||||||
|  |         path = Storage.get_names_path() | ||||||
|  |  | ||||||
|  |         with path.open() as file: | ||||||
|  |             return json.load(file) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_settings() -> Any: | ||||||
|  |         try: | ||||||
|  |             with Config.settings_json.open() as file: | ||||||
|  |                 return json.load(file) | ||||||
|  |         except Exception as e: | ||||||
|  |             Utils.print(str(e)) | ||||||
|  |             return {} | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def save_settings(settings: dict[str, Any]) -> None: | ||||||
|  |         with Config.settings_json.open("w") as file: | ||||||
|  |             json.dump(settings, file) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_countries() -> Any: | ||||||
|  |         with Config.countries_json.open() as file: | ||||||
|  |             return json.load(file) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_manifest() -> Any: | ||||||
|  |         with Config.manifest_path.open() as file: | ||||||
|  |             return json.load(file) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def save_arguments(text: str) -> None: | ||||||
|  |         with Config.arguments_path.open("w") as file: | ||||||
|  |             file.write(text) | ||||||
							
								
								
									
										221
									
								
								cromulant/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,221 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import random | ||||||
|  | import colorsys | ||||||
|  | import time | ||||||
|  | from datetime import datetime | ||||||
|  | from typing import ClassVar | ||||||
|  |  | ||||||
|  | from wonderwords import RandomWord, RandomSentence  # type: ignore | ||||||
|  | from fontTools.ttLib import TTFont  # type: ignore | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Utils: | ||||||
|  |     names: ClassVar[list[str]] = [] | ||||||
|  |     countries: ClassVar[list[str]] = [] | ||||||
|  |     rand_word: RandomWord | ||||||
|  |     rand_sentence: RandomSentence | ||||||
|  |     vowels = "aeiou" | ||||||
|  |     consonants = "bcdfghjklmnpqrstvwxyz" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         from .storage import Storage | ||||||
|  |  | ||||||
|  |         Utils.names = Storage.get_names() | ||||||
|  |         Utils.countries = Storage.get_countries() | ||||||
|  |         Utils.rand_word = RandomWord() | ||||||
|  |         Utils.rand_sentence = RandomSentence() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def now() -> int: | ||||||
|  |         return int(time.time()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def singular_or_plural(num: float, singular: str, plural: str) -> str: | ||||||
|  |         if num == 1: | ||||||
|  |             return singular | ||||||
|  |  | ||||||
|  |         return plural | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def time_ago(start_time: float, end_time: float) -> str: | ||||||
|  |         diff = end_time - start_time | ||||||
|  |         seconds = int(diff) | ||||||
|  |  | ||||||
|  |         if seconds < 60: | ||||||
|  |             word = Utils.singular_or_plural(seconds, "second", "seconds") | ||||||
|  |             return f"{seconds} {word} ago" | ||||||
|  |  | ||||||
|  |         minutes = seconds // 60 | ||||||
|  |  | ||||||
|  |         if minutes < 60: | ||||||
|  |             word = Utils.singular_or_plural(minutes, "minute", "minutes") | ||||||
|  |             return f"{minutes} {word} ago" | ||||||
|  |  | ||||||
|  |         hours = minutes / 60 | ||||||
|  |  | ||||||
|  |         if hours < 24: | ||||||
|  |             word = Utils.singular_or_plural(hours, "hour", "hours") | ||||||
|  |             return f"{hours:.1f} {word} ago" | ||||||
|  |  | ||||||
|  |         days = hours / 24 | ||||||
|  |  | ||||||
|  |         if days < 30: | ||||||
|  |             word = Utils.singular_or_plural(days, "day", "days") | ||||||
|  |             return f"{days:.1f} {word} ago" | ||||||
|  |  | ||||||
|  |         months = days / 30 | ||||||
|  |  | ||||||
|  |         if months < 12: | ||||||
|  |             word = Utils.singular_or_plural(months, "month", "months") | ||||||
|  |             return f"{months:.1f} {word} ago" | ||||||
|  |  | ||||||
|  |         years = months / 12 | ||||||
|  |         word = Utils.singular_or_plural(years, "year", "years") | ||||||
|  |         return f"{years:.1f} {word} ago" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def print(text: str) -> None: | ||||||
|  |         print(text)  # noqa: T201 | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_color(seed: str) -> tuple[int, int, int]: | ||||||
|  |         seed_int = hash(seed) | ||||||
|  |         random.seed(seed_int) | ||||||
|  |  | ||||||
|  |         h, s, l = ( | ||||||
|  |             random.random(), | ||||||
|  |             0.5 + random.random() / 2.0, | ||||||
|  |             0.4 + random.random() / 5.0, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         r, g, b = (int(256 * i) for i in colorsys.hls_to_rgb(h, l, s)) | ||||||
|  |         return r, g, b | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_name(ignore: list[str], include: list[str] | None = None) -> str: | ||||||
|  |         names = Utils.names | ||||||
|  |  | ||||||
|  |         if include: | ||||||
|  |             for name in include: | ||||||
|  |                 if name not in names: | ||||||
|  |                     names.append(name) | ||||||
|  |  | ||||||
|  |         filtered = [name for name in Utils.names if name not in ignore] | ||||||
|  |  | ||||||
|  |         if not filtered: | ||||||
|  |             return Utils.make_name() | ||||||
|  |  | ||||||
|  |         return random.choice(filtered) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_rgb(color: tuple[int, int, int]) -> str: | ||||||
|  |         return f"rgb{color}" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_character(font_path: str, num: int) -> str: | ||||||
|  |         font = TTFont(font_path) | ||||||
|  |         cmap = font["cmap"] | ||||||
|  |         unicode_map = cmap.getBestCmap() | ||||||
|  |         characters = [chr(code_point) for code_point in unicode_map] | ||||||
|  |  | ||||||
|  |         for _ in range(10):  # Try up to 10 times | ||||||
|  |             selected = random.sample(characters, num) | ||||||
|  |  | ||||||
|  |             if all((char.isprintable() and not char.isspace()) for char in selected): | ||||||
|  |                 return " ".join(selected) | ||||||
|  |  | ||||||
|  |         return "" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_emoji(num: int) -> str: | ||||||
|  |         return Utils.random_character(str(Config.emoji_font_path), num) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def to_date(timestamp: float) -> str: | ||||||
|  |         dt_object = datetime.fromtimestamp(timestamp) | ||||||
|  |         hour = dt_object.strftime("%I").lstrip("0") | ||||||
|  |         return dt_object.strftime(f"%b %d %Y - {hour}:%M %p") | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_timeword(minutes: float) -> str: | ||||||
|  |         if minutes < 1: | ||||||
|  |             seconds = round(minutes * 60) | ||||||
|  |  | ||||||
|  |             if seconds == 1: | ||||||
|  |                 return "1 second" | ||||||
|  |  | ||||||
|  |             if seconds < 60: | ||||||
|  |                 return f"{seconds} seconds" | ||||||
|  |  | ||||||
|  |         if minutes == 1: | ||||||
|  |             return "1 minute" | ||||||
|  |  | ||||||
|  |         return f"{round(minutes)} minutes" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_country(ignore: list[str]) -> str: | ||||||
|  |         filtered = [country for country in Utils.countries if country not in ignore] | ||||||
|  |         return random.choice(filtered) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_word(noun: bool = True, adj: bool = True) -> str: | ||||||
|  |         opts = [] | ||||||
|  |  | ||||||
|  |         if noun: | ||||||
|  |             opts.append("noun") | ||||||
|  |  | ||||||
|  |         if adj: | ||||||
|  |             opts.append("adjective") | ||||||
|  |  | ||||||
|  |         if not len(opts): | ||||||
|  |             return "" | ||||||
|  |  | ||||||
|  |         word = Utils.rand_word.word(include_parts_of_speech=opts, word_max_length=8) | ||||||
|  |         return str(word) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def random_words(num: int = 1, noun: bool = True, adj: bool = True) -> list[str]: | ||||||
|  |         return [Utils.random_word(noun=noun) for _ in range(num)] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def capitalize(word: str) -> str: | ||||||
|  |         return word[0].upper() + word[1:] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def words_1() -> str: | ||||||
|  |         return str(Utils.rand_sentence.simple_sentence()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def words_2() -> str: | ||||||
|  |         return str(Utils.rand_sentence.bare_bone_sentence()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def words_3() -> str: | ||||||
|  |         return str(Utils.rand_sentence.bare_bone_with_adjective()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def words_4() -> str: | ||||||
|  |         return str(Utils.rand_sentence.sentence()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_word() -> str: | ||||||
|  |         name = "" | ||||||
|  |         name += random.choice(Utils.consonants) | ||||||
|  |         name += random.choice(Utils.vowels) | ||||||
|  |         name += random.choice(Utils.consonants) | ||||||
|  |         name += random.choice(Utils.vowels) | ||||||
|  |         return name | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_words(num: int = 1) -> list[str]: | ||||||
|  |         return [Utils.make_word() for _ in range(num)] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make_name() -> str: | ||||||
|  |         words = Utils.make_words(2) | ||||||
|  |         words = [word.capitalize() for word in words] | ||||||
|  |         return " ".join(words) | ||||||
							
								
								
									
										445
									
								
								cromulant/window.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,445 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from typing import Any | ||||||
|  | from collections.abc import Callable | ||||||
|  | import signal | ||||||
|  |  | ||||||
|  | from PySide6.QtWidgets import QApplication  # type: ignore | ||||||
|  | from PySide6.QtWidgets import QDialog | ||||||
|  | from PySide6.QtWidgets import QMainWindow | ||||||
|  | from PySide6.QtWidgets import QWidget | ||||||
|  | from PySide6.QtWidgets import QGraphicsScene | ||||||
|  | from PySide6.QtWidgets import QVBoxLayout | ||||||
|  | from PySide6.QtWidgets import QLabel | ||||||
|  | from PySide6.QtWidgets import QPushButton | ||||||
|  | from PySide6.QtWidgets import QHBoxLayout | ||||||
|  | from PySide6.QtWidgets import QScrollArea | ||||||
|  | from PySide6.QtWidgets import QComboBox | ||||||
|  | from PySide6.QtWidgets import QLayout | ||||||
|  | from PySide6.QtWidgets import QSizePolicy | ||||||
|  | from PySide6.QtWidgets import QMessageBox | ||||||
|  | from PySide6.QtWidgets import QLineEdit | ||||||
|  | from PySide6.QtGui import QShortcut  # type: ignore | ||||||
|  | from PySide6.QtGui import QKeySequence | ||||||
|  | from PySide6.QtGui import QFontDatabase | ||||||
|  | from PySide6.QtGui import QIcon | ||||||
|  | from PySide6.QtGui import QKeyEvent | ||||||
|  | from PySide6.QtGui import QMouseEvent | ||||||
|  | from PySide6.QtCore import Qt  # type: ignore | ||||||
|  | from PySide6.QtCore import QUrl | ||||||
|  | from PySide6.QtCore import Signal | ||||||
|  | from PySide6.QtMultimedia import QMediaPlayer  # type: ignore | ||||||
|  | from PySide6.QtMultimedia import QAudioOutput | ||||||
|  |  | ||||||
|  | from .config import Config | ||||||
|  | from .args import Args | ||||||
|  | from .utils import Utils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SpecialButton(QPushButton):  # type: ignore | ||||||
|  |     middleClicked = Signal() | ||||||
|  |  | ||||||
|  |     def __init__(self, *args: Any, **kwargs: Any) -> None: | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     def mousePressEvent(self, e: QMouseEvent) -> None: | ||||||
|  |         if e.button() == Qt.MiddleButton: | ||||||
|  |             self.middleClicked.emit() | ||||||
|  |         else: | ||||||
|  |             super().mousePressEvent(e) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SpecialComboBox(QComboBox):  # type: ignore | ||||||
|  |     middleClicked = Signal() | ||||||
|  |  | ||||||
|  |     def __init__(self, *args: Any, **kwargs: Any) -> None: | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     def mousePressEvent(self, e: QMouseEvent) -> None: | ||||||
|  |         if e.button() == Qt.MiddleButton: | ||||||
|  |             self.middleClicked.emit() | ||||||
|  |         else: | ||||||
|  |             super().mousePressEvent(e) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FilterLineEdit(QLineEdit):  # type: ignore | ||||||
|  |     def keyPressEvent(self, e: QKeyEvent) -> None: | ||||||
|  |         if e.key() == Qt.Key_Escape: | ||||||
|  |             self.clear() | ||||||
|  |         else: | ||||||
|  |             super().keyPressEvent(e) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RestartDialog(QDialog):  # type: ignore | ||||||
|  |     def __init__(self, sizes: list[str], defindex: int) -> None: | ||||||
|  |         super().__init__() | ||||||
|  |         self.setWindowTitle("Select Option") | ||||||
|  |         self.setFixedSize(300, 150) | ||||||
|  |  | ||||||
|  |         self.layout = QVBoxLayout() | ||||||
|  |  | ||||||
|  |         self.label = QLabel("Size of the population") | ||||||
|  |         self.layout.addWidget(self.label) | ||||||
|  |  | ||||||
|  |         self.size_combo = QComboBox() | ||||||
|  |         self.size_combo.addItems(sizes) | ||||||
|  |         self.size_combo.setCurrentIndex(defindex) | ||||||
|  |         self.layout.addWidget(self.size_combo) | ||||||
|  |  | ||||||
|  |         self.button_layout = QHBoxLayout() | ||||||
|  |  | ||||||
|  |         self.cancel_button = QPushButton("Cancel") | ||||||
|  |         self.cancel_button.clicked.connect(self.reject) | ||||||
|  |         self.button_layout.addWidget(self.cancel_button) | ||||||
|  |  | ||||||
|  |         self.ok_button = QPushButton("OK") | ||||||
|  |         self.ok_button.clicked.connect(self.accept) | ||||||
|  |         self.ok_button.setDefault(True) | ||||||
|  |         self.button_layout.addWidget(self.ok_button) | ||||||
|  |  | ||||||
|  |         self.layout.addLayout(self.button_layout) | ||||||
|  |         self.setLayout(self.layout) | ||||||
|  |         self.setWindowFlags(Qt.Popup) | ||||||
|  |  | ||||||
|  |     def get_data(self) -> dict[str, Any]: | ||||||
|  |         return { | ||||||
|  |             "size": str(self.size_combo.currentText()), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Window: | ||||||
|  |     app: QApplication | ||||||
|  |     window: QMainWindow | ||||||
|  |     root: QVBoxLayout | ||||||
|  |     view: QVBoxLayout | ||||||
|  |     view_scene: QGraphicsScene | ||||||
|  |     speed: QComboBox | ||||||
|  |     scroll_area: QScrollArea | ||||||
|  |     info: QPushButton | ||||||
|  |     font: str | ||||||
|  |     emoji_font: str | ||||||
|  |     mono_font: str | ||||||
|  |     player: QMediaPlayer | ||||||
|  |     audio: QAudioOutput | ||||||
|  |     filter: QLineEdit | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def prepare() -> None: | ||||||
|  |         Window.make() | ||||||
|  |         Window.add_buttons() | ||||||
|  |         Window.add_view() | ||||||
|  |         Window.add_footer() | ||||||
|  |         Window.setup_keyboard() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def make() -> None: | ||||||
|  |         Window.app = QApplication([]) | ||||||
|  |         program = Args.program or Config.program | ||||||
|  |         Window.app.setApplicationName(program) | ||||||
|  |  | ||||||
|  |         Window.window = QMainWindow() | ||||||
|  |         title = Args.title or Config.title | ||||||
|  |         Window.window.setWindowTitle(title) | ||||||
|  |         width = Args.width or Config.width | ||||||
|  |         height = Args.height or Config.height | ||||||
|  |         Window.window.resize(width, height) | ||||||
|  |  | ||||||
|  |         central_widget = QWidget() | ||||||
|  |         Window.root = QVBoxLayout() | ||||||
|  |         central_widget.setLayout(Window.root) | ||||||
|  |         Window.root.setAlignment(Qt.AlignTop) | ||||||
|  |         Window.window.setCentralWidget(central_widget) | ||||||
|  |         Window.window.setWindowIcon(QIcon(str(Config.icon_path))) | ||||||
|  |         Window.root.setContentsMargins(0, 0, 0, 0) | ||||||
|  |         Window.set_style() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_style() -> None: | ||||||
|  |         font_id = QFontDatabase.addApplicationFont(str(Config.font_path)) | ||||||
|  |         emoji_font_id = QFontDatabase.addApplicationFont(str(Config.emoji_font_path)) | ||||||
|  |         mono_font_id = QFontDatabase.addApplicationFont(str(Config.mono_font_path)) | ||||||
|  |  | ||||||
|  |         if font_id != -1: | ||||||
|  |             Window.font = QFontDatabase.applicationFontFamilies(font_id)[0] | ||||||
|  |  | ||||||
|  |         if emoji_font_id != -1: | ||||||
|  |             Window.emoji_font = QFontDatabase.applicationFontFamilies(emoji_font_id)[0] | ||||||
|  |  | ||||||
|  |         if mono_font_id != -1: | ||||||
|  |             Window.mono_font = QFontDatabase.applicationFontFamilies(mono_font_id)[0] | ||||||
|  |  | ||||||
|  |         style = f""" | ||||||
|  |  | ||||||
|  |         QWidget {{ | ||||||
|  |             background-color: {Config.background_color}; | ||||||
|  |             color: {Config.text_color}; | ||||||
|  |             font-size: {Config.font_size}px; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QMenu {{ | ||||||
|  |             background-color: {Config.alt_background_color}; | ||||||
|  |             color: {Config.alt_text_color}; | ||||||
|  |             border: 2px solid {Config.alt_border_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QMenu::item:selected {{ | ||||||
|  |             background-color: {Config.alt_hover_background_color}; | ||||||
|  |             color: {Config.alt_hover_text_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QDialog {{ | ||||||
|  |             background-color: {Config.alt_background_color}; | ||||||
|  |             color: {Config.alt_text_color}; | ||||||
|  |             border: 2px solid {Config.alt_border_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QDialog QLabel {{ | ||||||
|  |             background-color: {Config.alt_background_color}; | ||||||
|  |             color: {Config.alt_text_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QDialog QPushButton {{ | ||||||
|  |             background-color: {Config.alt_background_color}; | ||||||
|  |             color: {Config.alt_text_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QDialog QPushButton:hover {{ | ||||||
|  |             background-color: {Config.message_box_button_hover_background_color}; | ||||||
|  |             color: {Config.message_box_button_hover_text_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QComboBox {{ | ||||||
|  |             selection-background-color: {Config.alt_hover_background_color}; | ||||||
|  |             selection-color: {Config.alt_hover_text_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QComboBox QAbstractItemView {{ | ||||||
|  |             background-color: {Config.alt_background_color}; | ||||||
|  |             color: {Config.alt_text_color}; | ||||||
|  |             border: 2px solid {Config.alt_border_color}; | ||||||
|  |             padding: 6px; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QScrollBar:vertical {{ | ||||||
|  |             border: 0px solid transparent; | ||||||
|  |             background: {Config.background_color}; | ||||||
|  |             width: 15px; | ||||||
|  |             margin: 0px 0px 0px 0px; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QScrollBar::handle:vertical {{ | ||||||
|  |             background: {Config.scrollbar_handle_color}; | ||||||
|  |             min-height: 20px; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QScrollBar::add-line:vertical {{ | ||||||
|  |             border: none; | ||||||
|  |             background: none; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QScrollBar::sub-line:vertical {{ | ||||||
|  |             border: none; | ||||||
|  |             background: none; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QLineEdit {{ | ||||||
|  |             background-color: {Config.input_background_color}; | ||||||
|  |             color: {Config.input_text_color}; | ||||||
|  |             border: 1px solid {Config.input_border_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QFrame#horizontal_line {{ | ||||||
|  |             background-color: white; | ||||||
|  |             color: white; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         QLabel#menu_label:hover {{ | ||||||
|  |             background-color: {Config.alt_hover_background_color}; | ||||||
|  |         }} | ||||||
|  |  | ||||||
|  |         """.strip() | ||||||
|  |  | ||||||
|  |         Window.app.setStyleSheet(style) | ||||||
|  |  | ||||||
|  |         if Args.mono: | ||||||
|  |             Window.app.setFont(Window.mono_font) | ||||||
|  |         else: | ||||||
|  |             Window.app.setFont(Window.font) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_buttons() -> None: | ||||||
|  |         from .game import Game | ||||||
|  |         from .filter import Filter | ||||||
|  |  | ||||||
|  |         root = QWidget() | ||||||
|  |         container = QHBoxLayout() | ||||||
|  |  | ||||||
|  |         btn_menu = SpecialButton("Menu") | ||||||
|  |         btn_menu.setToolTip("The main menu\nMiddle Click: Update") | ||||||
|  |         btn_menu.clicked.connect(Game.menu) | ||||||
|  |         btn_menu.middleClicked.connect(Game.force_update) | ||||||
|  |  | ||||||
|  |         Window.speed = SpecialComboBox() | ||||||
|  |         tooltip = "The speed of the updates\n" | ||||||
|  |  | ||||||
|  |         fast = (Args.fast_seconds or Config.fast_seconds) / 60 | ||||||
|  |         tooltip += f"Fast: {Utils.get_timeword(fast)}\n" | ||||||
|  |  | ||||||
|  |         normal = Args.normal_minutes or Config.normal_minutes | ||||||
|  |         tooltip += f"Normal: {Utils.get_timeword(normal)}\n" | ||||||
|  |  | ||||||
|  |         slow = Args.slow_minutes or Config.slow_minutes | ||||||
|  |         tooltip += f"Slow: {Utils.get_timeword(slow)}\n" | ||||||
|  |  | ||||||
|  |         tooltip += "Middle Click: Slow" | ||||||
|  |         Window.speed.setToolTip(tooltip) | ||||||
|  |         Window.speed.addItems(["Fast", "Normal", "Slow", "Paused"]) | ||||||
|  |         Window.speed.setCurrentIndex(1) | ||||||
|  |         Window.speed.currentIndexChanged.connect(Game.update_speed) | ||||||
|  |         Window.speed.middleClicked.connect(Game.slowdown) | ||||||
|  |  | ||||||
|  |         Window.filter = FilterLineEdit() | ||||||
|  |         Window.filter.setPlaceholderText("Filter") | ||||||
|  |         Window.filter.mousePressEvent = lambda e: Window.to_top() | ||||||
|  |         Window.filter.keyReleaseEvent = lambda e: Filter.filter(e) | ||||||
|  |  | ||||||
|  |         container.addWidget(btn_menu, 1) | ||||||
|  |         container.addWidget(Window.speed, 1) | ||||||
|  |         container.addWidget(Window.filter, 1) | ||||||
|  |  | ||||||
|  |         root.setLayout(container) | ||||||
|  |  | ||||||
|  |         if not Args.header: | ||||||
|  |             root.setVisible(False) | ||||||
|  |  | ||||||
|  |         Window.root.addWidget(root) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_view() -> None: | ||||||
|  |         Window.scroll_area = QScrollArea() | ||||||
|  |         Window.scroll_area.setWidgetResizable(True) | ||||||
|  |  | ||||||
|  |         container = QWidget() | ||||||
|  |         parent = QVBoxLayout(container) | ||||||
|  |         Window.view = QVBoxLayout() | ||||||
|  |         parent.addLayout(Window.view) | ||||||
|  |  | ||||||
|  |         Window.view.setAlignment(Qt.AlignTop) | ||||||
|  |         Window.scroll_area.setWidget(container) | ||||||
|  |         Window.root.addWidget(Window.scroll_area) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def start() -> None: | ||||||
|  |         signal.signal(signal.SIGINT, signal.SIG_DFL) | ||||||
|  |         Window.window.show() | ||||||
|  |         Window.app.exec() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def close() -> None: | ||||||
|  |         Window.app.quit() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def delete_layout(layout: QLayout) -> None: | ||||||
|  |         while layout.count(): | ||||||
|  |             item = layout.takeAt(0) | ||||||
|  |             if item.widget(): | ||||||
|  |                 item.widget().deleteLater() | ||||||
|  |             elif item.layout(): | ||||||
|  |                 Window.delete_layout(item.layout()) | ||||||
|  |  | ||||||
|  |         layout.deleteLater() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def expand(widget: QWidget) -> None: | ||||||
|  |         widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def expand_2(widget: QWidget) -> None: | ||||||
|  |         widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def clear_view() -> None: | ||||||
|  |         while Window.view.count(): | ||||||
|  |             item = Window.view.takeAt(0) | ||||||
|  |             if item.widget(): | ||||||
|  |                 item.widget().deleteLater() | ||||||
|  |             elif item.layout(): | ||||||
|  |                 Window.delete_layout(item.layout()) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def to_top() -> None: | ||||||
|  |         Window.scroll_area.verticalScrollBar().setValue(0) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def to_bottom() -> None: | ||||||
|  |         Window.scroll_area.verticalScrollBar().setValue( | ||||||
|  |             Window.scroll_area.verticalScrollBar().maximum() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def toggle_scroll() -> None: | ||||||
|  |         maxim = Window.scroll_area.verticalScrollBar().maximum() | ||||||
|  |  | ||||||
|  |         if Window.scroll_area.verticalScrollBar().value() == maxim: | ||||||
|  |             Window.to_top() | ||||||
|  |         else: | ||||||
|  |             Window.to_bottom() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def add_footer() -> None: | ||||||
|  |         from .game import Game | ||||||
|  |  | ||||||
|  |         root = QWidget() | ||||||
|  |         root.setContentsMargins(0, 0, 0, 0) | ||||||
|  |         container = QHBoxLayout() | ||||||
|  |         Window.info = SpecialButton("---") | ||||||
|  |  | ||||||
|  |         Window.info.setToolTip( | ||||||
|  |             "Click to scroll to the bottom or top\nMiddle Click: Filter Top" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         Window.info.clicked.connect(Window.toggle_scroll) | ||||||
|  |         Window.info.middleClicked.connect(Game.filter_top) | ||||||
|  |         Window.info.setMinimumSize(35, 35) | ||||||
|  |         container.addWidget(Window.info) | ||||||
|  |         root.setLayout(container) | ||||||
|  |  | ||||||
|  |         if not Args.footer: | ||||||
|  |             root.setVisible(False) | ||||||
|  |  | ||||||
|  |         Window.root.addWidget(root) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def play_audio(path: str, on_stop: Callable[..., Any] | None = None) -> None: | ||||||
|  |         Window.player = QMediaPlayer() | ||||||
|  |         Window.audio = QAudioOutput() | ||||||
|  |         Window.player.setAudioOutput(Window.audio) | ||||||
|  |         Window.player.setSource(QUrl.fromLocalFile(path)) | ||||||
|  |         Window.audio.setVolume(100) | ||||||
|  |  | ||||||
|  |         def handle_state_change(state: QMediaPlayer.State) -> None: | ||||||
|  |             if state == QMediaPlayer.StoppedState: | ||||||
|  |                 if on_stop: | ||||||
|  |                     on_stop() | ||||||
|  |  | ||||||
|  |         Window.player.playbackStateChanged.connect(handle_state_change) | ||||||
|  |         Window.player.play() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def stop_audio() -> None: | ||||||
|  |         Window.player.stop() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def alert(message: str) -> None: | ||||||
|  |         msg_box = QMessageBox() | ||||||
|  |         msg_box.setWindowFlags(Qt.Popup) | ||||||
|  |         msg_box.setIcon(QMessageBox.Information) | ||||||
|  |         msg_box.setText(message) | ||||||
|  |         msg_box.setWindowTitle("Information") | ||||||
|  |         msg_box.setStandardButtons(QMessageBox.Ok) | ||||||
|  |         msg_box.exec() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def setup_keyboard() -> None: | ||||||
|  |         on_enter = QShortcut(QKeySequence(Qt.Key_Return), Window.window) | ||||||
|  |         on_enter.activated.connect(Window.toggle_scroll) | ||||||
							
								
								
									
										25
									
								
								more.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | |||||||
|  | ## Propaganda | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Assets | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Soundtrack | ||||||
|  |  | ||||||
|  | [March of The Cyber Ants](cromulant/audio/March%20of%20the%20Cyber%20Ants.mp3) | ||||||
							
								
								
									
										5
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | PySide6 == 6.7.2 | ||||||
|  | appdirs == 1.4.4 | ||||||
|  | wonderwords == 2.2.0 | ||||||
|  | fonttools == 4.53.1 | ||||||
|  | pre-commit == 3.7.1 | ||||||
							
								
								
									
										6
									
								
								run.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  |  | ||||||
|  | root="$(dirname "$(readlink -f "$0")")" | ||||||
|  | cd "$root" | ||||||
|  |  | ||||||
|  | venv/bin/python -m cromulant.main "$@" | ||||||
							
								
								
									
										9
									
								
								screenshots.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										69
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | |||||||
|  | from setuptools import setup, find_packages | ||||||
|  | from pathlib import Path | ||||||
|  | import shutil | ||||||
|  | import json | ||||||
|  | import platform | ||||||
|  |  | ||||||
|  | with open("cromulant/manifest.json", "r") as file: | ||||||
|  |     manifest = json.load(file) | ||||||
|  |  | ||||||
|  | title = manifest["title"] | ||||||
|  | program = manifest["program"] | ||||||
|  | version = manifest["version"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _post_install(): | ||||||
|  |     system = platform.system() | ||||||
|  |  | ||||||
|  |     if system == "Linux": | ||||||
|  |         try: | ||||||
|  |             _copy_icon_file() | ||||||
|  |             _create_desktop_file() | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error during post install: {e}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _copy_icon_file(): | ||||||
|  |     source = Path(f"{program}/img/icon_1.jpg").expanduser().resolve() | ||||||
|  |     destination = Path(f"~/.local/share/icons/{program}.png").expanduser().resolve() | ||||||
|  |     shutil.copy2(source, destination) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _create_desktop_file(): | ||||||
|  |     content = f"""[Desktop Entry] | ||||||
|  | Version=1.0 | ||||||
|  | Name={title} | ||||||
|  | Exec={Path(f"~/.local/bin/{program}").expanduser().resolve()} | ||||||
|  | Icon={Path(f"~/.local/share/icons/{program}.png").expanduser().resolve()} | ||||||
|  | Terminal=false | ||||||
|  | Type=Application | ||||||
|  | Categories=Utility; | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |     file_path = Path(f"~/.local/share/applications/{program}.desktop").expanduser().resolve() | ||||||
|  |  | ||||||
|  |     with open(file_path, 'w') as f: | ||||||
|  |         f.write(content) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | with open("requirements.txt") as f: | ||||||
|  |     requirements = f.read().splitlines() | ||||||
|  |  | ||||||
|  | package_data = {} | ||||||
|  | package_data[program] = ["**/*.png", "**/*.jpg", "**/*.json", "**/*.mp3", "**/*.ttf"] | ||||||
|  |  | ||||||
|  | setup( | ||||||
|  |     name=title, | ||||||
|  |     version=version, | ||||||
|  |     install_requires=requirements, | ||||||
|  |     packages=find_packages(where="."), | ||||||
|  |     package_dir={"": "."}, | ||||||
|  |     package_data=package_data, | ||||||
|  |     entry_points={ | ||||||
|  |         "console_scripts": [ | ||||||
|  |             f"{program}={program}.main:main", | ||||||
|  |         ], | ||||||
|  |     }, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | _post_install() | ||||||
							
								
								
									
										23
									
								
								utils/tag.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  |  | ||||||
|  | # This is used to create a tag in the git repo | ||||||
|  | # You probably don't want to run this | ||||||
|  |  | ||||||
|  | # pacman: python-gitpython | ||||||
|  | import os | ||||||
|  | import git | ||||||
|  | import json | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | here = Path(__file__).resolve() | ||||||
|  | parent = here.parent.parent | ||||||
|  | os.chdir(parent) | ||||||
|  |  | ||||||
|  | with open("cromulant/manifest.json") as f: | ||||||
|  |     manifest = json.loads(f.read()) | ||||||
|  |  | ||||||
|  | version = manifest["version"] | ||||||
|  | repo = git.Repo(".") | ||||||
|  | repo.create_tag(version) | ||||||
|  | repo.remotes.origin.push(version) | ||||||
|  | print(f"Created tag: {version}") | ||||||
 Auric Vente
					Auric Vente