first commit
|
@ -0,0 +1,6 @@
|
||||||
|
venv/*
|
||||||
|
.directory
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.mypy_cache/
|
||||||
|
cromulant.db
|
|
@ -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/
|
|
@ -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)
|
|
@ -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,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)
|
|
@ -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("_", "-")
|
|
@ -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",
|
||||||
|
)
|
|
@ -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"
|
|
@ -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"
|
||||||
|
]
|
|
@ -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()
|
|
@ -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
|
After Width: | Height: | Size: 306 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
convert $1 -channel G -evaluate add 16% +channel -quality 85 -strip $1_green
|
After Width: | Height: | Size: 292 KiB |
After Width: | Height: | Size: 492 KiB |
After Width: | Height: | Size: 165 KiB |
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 153 KiB |
After Width: | Height: | Size: 146 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
convert $1 -channel R -evaluate add 16% +channel -quality 85 -strip $1_red
|
After Width: | Height: | Size: 271 KiB |
After Width: | Height: | Size: 284 KiB |
After Width: | Height: | Size: 415 KiB |
|
@ -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()
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"version": "4.2.0",
|
||||||
|
"title": "Cromulant",
|
||||||
|
"program": "cromulant",
|
||||||
|
"author": "madprops",
|
||||||
|
"repo": "github.com/madprops/cromulant",
|
||||||
|
"description": "Toy game about ants"
|
||||||
|
}
|
|
@ -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",
|
||||||
|
]
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,25 @@
|
||||||
|
## Propaganda
|
||||||
|
|
||||||
|
![](cromulant/img/logo_1.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/logo_2.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/logo_3.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/logo_4.jpg)
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
![](cromulant/img/status.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/hatched.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/terminated.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/change.jpg)
|
||||||
|
|
||||||
|
![](cromulant/img/top.jpg)
|
||||||
|
|
||||||
|
## Soundtrack
|
||||||
|
|
||||||
|
[March of The Cyber Ants](cromulant/audio/March%20of%20the%20Cyber%20Ants.mp3)
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
root="$(dirname "$(readlink -f "$0")")"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
venv/bin/python -m cromulant.main "$@"
|
|
@ -0,0 +1,9 @@
|
||||||
|
![](https://i.imgur.com/5yIX4hl.jpeg)
|
||||||
|
|
||||||
|
![](https://i.imgur.com/e48HxaC.jpeg)
|
||||||
|
|
||||||
|
![](https://i.imgur.com/Y66isBl.jpeg)
|
||||||
|
|
||||||
|
![](https://i.imgur.com/qZnnxoB.jpeg)
|
||||||
|
|
||||||
|
![](https://i.imgur.com/7O5BP5K.jpeg)
|
|
@ -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()
|
|
@ -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}")
|