from __future__ import annotations import random from typing import Any 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.QtGui import QCursor # type: ignore from PySide6.QtGui import QMouseEvent from PySide6.QtGui import QPixmap from PySide6.QtGui import QAction from PySide6.QtCore import QSize # type: ignore 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_1 = Opt(2, Method.think) think_2 = Opt(2, Method.think) words_1 = Opt(3, Method.words) words_2 = Opt(3, Method.words) words_3 = Opt(3, Method.words) words_4 = Opt(3, 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_1, Opts.think_2] @staticmethod def opts_words() -> list[Opt]: return [ Opts.words_1, Opts.words_2, Opts.words_3, Opts.words_4, ] class Game: timer: QTimer | None = None playing_song: bool = False merge_charge: int = 0 @staticmethod def prepare() -> None: 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 Window.view.insertWidget(0, item) 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 == Ants.top: path = Config.top_image_path elif ant.method == "hatched": path = Config.hatched_image_path elif ant.method == "terminated": path = Config.terminated_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_4.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_1.value: status = Utils.random_name([], Ants.get_names()) method = Opts.think_1.method elif value == Opts.think_2.value: status = Utils.random_emoji(3) method = Opts.think_2.method elif value == Opts.words_1.value: status = Utils.words_1() method = Opts.words_1.method elif value == Opts.words_2.value: status = Utils.words_2() method = Opts.words_2.method elif value == Opts.words_3.value: status = Utils.words_3() method = Opts.words_3.method elif value >= Opts.words_4.value: status = Utils.words_4() method = Opts.words_4.method 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: if Game.timer: Game.timer.stop() speed = Settings.speed if speed == "fast": delay = Config.loop_delay_fast elif speed == "normal": delay = Config.loop_delay_normal elif speed == "slow": delay = Config.loop_delay_slow else: return Game.timer = QTimer() Game.timer.timeout.connect(Game.get_status) Game.timer.start(delay) @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]) Window.clear_view() Ants.populate(size) Window.to_top() Game.intro() Game.start_loop() @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}") if Settings.merge: merge = make("Merge", True) else: merge = make("Merge", False) if Settings.score_enabled: score = make("Score", True) else: score = make("Score", False) if Settings.travel_enabled: travel = make("Travel", True) else: travel = make("Travel", False) if Settings.think_enabled: think = make("Think", True) else: think = make("Think", False) if Settings.words_enabled: words = make("Words", True) else: words = make("Words", False) 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: 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)