cromulant/cromulant/game.py

529 lines
14 KiB
Python

from __future__ import annotations
import random
from typing import 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.QtGui import QCursor # type: ignore
from PySide6.QtGui import QMouseEvent
from PySide6.QtGui import QPixmap
from PySide6.QtGui import QAction
from PySide6.QtCore import QTimer # type: ignore
from PySide6.QtCore import Qt
from .config import Config
from .utils import Utils
from .ants import Ant
from .ants import Ants
from .window import Window
from .settings import Settings
class Method:
merge: ClassVar[dict[str, int]] = {"value": 0, "weight": 1}
triumph: ClassVar[dict[str, int]] = {"value": 1, "weight": 2}
hit: ClassVar[dict[str, int]] = {"value": 2, "weight": 2}
travel: ClassVar[dict[str, int]] = {"value": 3, "weight": 2}
thinking_1: ClassVar[dict[str, int]] = {"value": 4, "weight": 2}
thinking_2: ClassVar[dict[str, int]] = {"value": 5, "weight": 2}
sentence_1: ClassVar[dict[str, int]] = {"value": 6, "weight": 3}
sentence_2: ClassVar[dict[str, int]] = {"value": 7, "weight": 3}
sentence_3: ClassVar[dict[str, int]] = {"value": 8, "weight": 3}
sentence_4: ClassVar[dict[str, int]] = {"value": 9, "weight": 3}
Opts = list[dict[str, int]]
@staticmethod
def opts_score() -> Opts:
return [Method.triumph, Method.hit]
@staticmethod
def opts_travel() -> Opts:
return [Method.travel]
@staticmethod
def opts_thought() -> Opts:
return [Method.thinking_1, Method.thinking_2]
@staticmethod
def opts_words() -> Opts:
return [
Method.sentence_1,
Method.sentence_2,
Method.sentence_3,
Method.sentence_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)
image_label = Game.get_image(ant)
right_container = Game.make_right_container(ant)
container.addWidget(image_label)
container.addSpacing(Config.space_1)
container.addWidget(right_container)
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)
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.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)
image_label.setFixedSize(scaled_pixmap.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[dict[str, int]] = []
if Settings.score_enabled:
opts.extend(Method.opts_score())
if Settings.travel_enabled:
opts.extend(Method.opts_travel())
if Settings.think_enabled:
opts.extend(Method.opts_thought())
if Settings.words_enabled:
opts.extend(Method.opts_words())
if not opts:
return
nums = [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 = Method.merge
nums.insert(0, opt["value"])
weights.insert(0, opt["weight"])
num = random.choices(nums, weights=weights, k=1)[0]
if num == Method.merge["value"]:
if Ants.merge():
Game.merge_charge = 0
return
num = Method.sentence_4["value"]
status = ""
method = "normal"
if num == Method.triumph["value"]:
ant.triumph += 1
method = "triumph"
elif num == Method.hit["value"]:
ant.hits += 1
method = "hit"
elif num == Method.travel["value"]:
status = Utils.random_country([])
method = "travel"
elif num == Method.thinking_1["value"]:
status = Utils.random_name([], Ants.get_names())
method = "thinking"
elif num == Method.thinking_2["value"]:
status = Utils.random_emoji(3)
method = "thinking"
elif num == Method.sentence_1["value"]:
status = Utils.rand_sentence.simple_sentence()
elif num == Method.sentence_2["value"]:
status = Utils.rand_sentence.bare_bone_sentence()
elif num == Method.sentence_3["value"]:
status = Utils.rand_sentence.bare_bone_with_adjective()
elif num >= Method.sentence_4["value"]:
status = Utils.rand_sentence.sentence()
else:
status = "???"
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.get_top()
if top:
ant = top[0]
score = top[1]
text.append(f"Top:{nb}{ant.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:
opts = ["25", "50", "100", "250"]
defindex = 0
for i, opt in enumerate(opts):
if int(opt) == Config.default_population:
defindex = i
break
opts = [f"{opt} ants" for opt in opts]
size = Window.prompt_combobox("Size of the population", opts, defindex)
if not size:
return
num = int(size.split(" ")[0])
Window.clear_view()
Ants.populate(num)
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:
version = Config.manifest["version"]
Game.message(f"Welcome to Cromulant v{version}")
@staticmethod
def menu() -> None:
menu = QMenu(Window.root.widget())
style = f"""
QMenu::separator {{
background-color: {Config.separator_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"Cromulant v{Config.manifest["version"]}",
"Listen to the ants and watch them go.",
"Just run it and leave it open on your screen.",
"5% of revenue goes to the local ant shelter.",
]
Window.alert("\n\n".join(lines))