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}")
|