564 lines
15 KiB
Python
564 lines
15 KiB
Python
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()
|
|
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))
|