diff --git a/game.md b/game.md new file mode 100644 index 0000000..322ef02 --- /dev/null +++ b/game.md @@ -0,0 +1,28 @@ +Round 1 +- Item name is announced in chat +- First person to find in inventory and send wins 1 point + +Round 2 +- Clues are given that relate to the item +- Person that guess the item win 1 point +- Then wait 1 min to showcase highest quantity +- Person with the most item quantity win 1 point + +### Items + +Round 1 +1. Hero's Radiance +2. Jellyfish +3. Medal of Light (quest) +4. S2 Exquisite box +5. Focus Potion +6. Toy - ee chan fireworks + +Round 2 +1. Mixing Agent +2. Cheer up treat +3. Gold quicksand +4. Emerald apple +5. Honey +6. Slumberdream feathers +7. Lionfish \ No newline at end of file diff --git a/src/inventory_wars/const.py b/src/inventory_wars/const.py index 3cee5eb..730d4ff 100644 --- a/src/inventory_wars/const.py +++ b/src/inventory_wars/const.py @@ -21,6 +21,7 @@ __all__ = ( "ChitChatNtf", "HypertextVariant", "decode_placeholder", + "ITEM_MAPPINGS", "get_item_name" ) @@ -75,11 +76,11 @@ def decode_placeholder(placeholder: PlaceHolder) -> SupportedPlaceholders: return decoder.FromString(placeholder.bytesContent) # type: ignore[attr-defined] -ITEM_NAME = pl.read_json("./ref/StarResonanceData/ztable/ItemTable.json").transpose().unnest() +ITEM_MAPPINGS = pl.read_json("./ref/StarResonanceData/ztable/ItemTable.json").transpose().unnest() def get_item_name(item_config_id: int) -> str | None: try: - return ITEM_NAME.filter(pl.col.Id == item_config_id).select("Name").item() + return ITEM_MAPPINGS.filter(pl.col.Id == item_config_id).select("Name").item() except ValueError: return None diff --git a/src/inventory_wars/game.py b/src/inventory_wars/game.py index a844001..edc6945 100644 --- a/src/inventory_wars/game.py +++ b/src/inventory_wars/game.py @@ -31,6 +31,10 @@ class Game: self.scapy = get_sniffer(sniffer) self.scapy.start() + @property + def state(self) -> State: + return self._state + def set_state(self, state: State) -> None: self._state = state diff --git a/src/inventory_wars/gui/InventoryWars/IdleView.qml b/src/inventory_wars/gui/InventoryWars/IdleView.qml new file mode 100644 index 0000000..2036ef9 --- /dev/null +++ b/src/inventory_wars/gui/InventoryWars/IdleView.qml @@ -0,0 +1,106 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Shapes +import Qt.labs.qmlmodels + +import InventoryWars +import Style + +Rectangle { + id: root + + color: UIStyle.background + + required property ListModel rounds + property QtObject selectedRound + + signal roundSelected() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 20 + + Item { Layout.fillHeight: true } + + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 256 + + source: "images/modules.png" + fillMode: Image.PreserveAspectFit + } + + Label { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeXL + color: UIStyle.titletextColor + + text: "Start a round" + } + + ComboBox { + id: roundSelect + + Layout.alignment: Qt.AlignHCenter + + model: root.rounds + textRole: "name" + } + + Button { + Layout.alignment: Qt.AlignHCenter + + buttonColor: UIStyle.highlightColor + buttonBorderColor: UIStyle.highlightBorderColor + textColor: UIStyle.textColor + + text: "Start" + + onClicked: { + root.selectedRound = rounds.get(roundSelect.currentIndex) + roundSelected() + } + } + + TableView { + Layout.fillHeight: true + + columnSpacing: 1 + rowSpacing: 1 + + delegate: Rectangle { + border.width: 1 + implicitHeight: 50 + implicitWidth: 100 + + Text { + anchors.centerIn: parent + text: display + } + } + + model: TableModel { + rows: [ + { + User: "me", + Score: "1" + } + ] + + TableModelColumn { + display: "User" + } + + TableModelColumn { + display: "Score" + } + } + } + + Item { Layout.fillHeight: true } + } +} diff --git a/src/inventory_wars/gui/InventoryWars/Main.qml b/src/inventory_wars/gui/InventoryWars/Main.qml new file mode 100644 index 0000000..4c80814 --- /dev/null +++ b/src/inventory_wars/gui/InventoryWars/Main.qml @@ -0,0 +1,85 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Shapes +import Qt.labs.qmlmodels + +import InventoryWars + +ApplicationWindow { + height: 480 + title: "Inventory Wars" + visible: true + width: 640 + + ListModel { + id: rounds + + ListElement { + name: "Round 1" + mode: GameService.GameScoring.FirstGuess + item_id: 100 + item_name: "Item 1" + item_image: "" + clue: "Clue 1" + } + ListElement { + name: "Round 2" + mode: GameService.GameScoring.FirstGuess + item_id: 100 + item_name: "Item 2" + item_image: "" + clue: "Clue 2" + } + ListElement { + name: "Round 3" + mode: GameService.GameScoring.FirstGuess + item_id: 100 + item_name: "Item 3" + item_image: "" + clue: "Clue 3" + } + ListElement { + name: "Round 4" + mode: GameService.GameScoring.FirstGuess + item_id: 100 + item_name: "Item 4" + item_image: "" + clue: "Clue 4" + } + } + + IdleView { + id: idleView + + anchors.fill: parent + + rounds: rounds + selectedRound: rounds.get(0) + onRoundSelected: { + idleView.visible = false + ongoingView.start() + ongoingView.visible = true + } + } + + OngoingView { + id: ongoingView + + anchors.fill: parent + + visible: false + round: idleView.selectedRound + gameService: gameService + onClose: { + idleView.visible = true + ongoingView.visible = false + } + } + + GameService { + id: gameService + } +} + diff --git a/src/inventory_wars/gui/InventoryWars/OngoingView.qml b/src/inventory_wars/gui/InventoryWars/OngoingView.qml new file mode 100644 index 0000000..126493f --- /dev/null +++ b/src/inventory_wars/gui/InventoryWars/OngoingView.qml @@ -0,0 +1,188 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Shapes +import Qt.labs.qmlmodels + +import InventoryWars +import Style + +Rectangle { + id: root + + color: UIStyle.background + + required property GameService gameService + required property QtObject round + property QtObject guessed: QtObject { + property string username + property double seconds + } + property QtObject highest: QtObject { + property string username + property int count + } + + function start() { + gameService.start(round.item_id, round.mode) + } + + signal close() + + Connections { + target: root.gameService + + function onGuessed(username: str, seconds: double) { + root.guessed.username = username + root.guessed.seconds = seconds + } + + function onHighest(username: str, count: int) { + root.highest.username = username + root.highest.count = count + } + } + + ColumnLayout { + anchors.fill: parent + anchors.bottomMargin: 10 + spacing: 5 + + ToolBar { + Layout.fillWidth: true + Layout.minimumHeight: 35 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 5 + anchors.rightMargin: 5 + Layout.alignment: Qt.AlignVCenter + + Button { + Layout.preferredWidth: 25 + Layout.preferredHeight: 25 + + icon.name: "back" + icon.source: "icons/x.svg" + + onClicked: root.close() + } + + Item { + Layout.fillWidth: true + } + + Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + font.pixelSize: UIStyle.fontSizeM + font.bold: true + color: UIStyle.titletextColor + + text: root.round.name + } + + Item { + Layout.fillWidth: true + } + } + } + + Item { Layout.fillHeight: true } + + Label { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeL + font.bold: true + color: UIStyle.titletextColor + + text: "Current Item:" + } + + Text { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeM + color: UIStyle.textColor + + text: root.gameService.state == GameService.Ongoing ? "???" : root.round.item_name + } + + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 256 + Layout.preferredHeight: 256 + + source: root.gameService.state == GameService.Ongoing ? "images/unknown.png" : root.round.item_image + fillMode: Image.PreserveAspectFit + } + + Text { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeXL + font.bold: true + color: UIStyle.textColor + + text: "" + + Timer { + id: elapsedTimer + interval: 10 + running: root.gameService.state != GameService.Ended + repeat: true + + onTriggered: { + parent.text = root.gameService.elapsedSeconds.toFixed(2) + "s" + } + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeM + font.bold: true + color: UIStyle.textColor + visible: root.gameService.state == GameService.GameState.Hidden + + text: "Clue: " + root.round.clue + } + + Text { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeM + font.bold: true + color: UIStyle.textColor + visible: root.gameService.state != GameService.GameState.Hidden + + text: "Fastest guess: " + root.guessed.seconds + "s (by " + root.guessed.username + ")" + } + + Text { + Layout.alignment: Qt.AlignHCenter + + font.pixelSize: UIStyle.fontSizeM + font.bold: true + color: UIStyle.textColor + visible: root.gameService.state != GameService.GameState.Hidden && root.round.mode == GameService.GameScoring.FirstGuessThenHighestAmount + + text: "Highest count: " + root.highest.count + " (by " + root.highest.username + ")" + } + + Item { Layout.fillHeight: true } + + Button { + Layout.alignment: Qt.AlignHCenter + + visible: root.gameService.state != GameService.GameState.Ended + + text: "End" + + onClicked: root.gameService.end() + } + } +} \ No newline at end of file diff --git a/src/inventory_wars/gui/InventoryWars/icons/x.svg b/src/inventory_wars/gui/InventoryWars/icons/x.svg new file mode 100644 index 0000000..54fe3a3 --- /dev/null +++ b/src/inventory_wars/gui/InventoryWars/icons/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/inventory_wars/gui/InventoryWars/images/modules.png b/src/inventory_wars/gui/InventoryWars/images/modules.png new file mode 100644 index 0000000..812faca Binary files /dev/null and b/src/inventory_wars/gui/InventoryWars/images/modules.png differ diff --git a/src/inventory_wars/gui/InventoryWars/images/unknown.png b/src/inventory_wars/gui/InventoryWars/images/unknown.png new file mode 100644 index 0000000..9ea215a Binary files /dev/null and b/src/inventory_wars/gui/InventoryWars/images/unknown.png differ diff --git a/src/inventory_wars/gui/InventoryWars/qmldir b/src/inventory_wars/gui/InventoryWars/qmldir new file mode 100644 index 0000000..6fc9ea3 --- /dev/null +++ b/src/inventory_wars/gui/InventoryWars/qmldir @@ -0,0 +1,4 @@ +module InventoryWars +Main 1.0 Main.qml +IdleView 1.0 IdleView.qml +OngoingView 1.0 OngoingView.qml diff --git a/src/inventory_wars/gui/Style/Button.qml b/src/inventory_wars/gui/Style/Button.qml new file mode 100644 index 0000000..3855108 --- /dev/null +++ b/src/inventory_wars/gui/Style/Button.qml @@ -0,0 +1,48 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T + +T.Button { + id: control + + property alias buttonColor: rect.color + property alias buttonBorderColor: rect.border.color + property alias textColor: label.color + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: 15 + rightPadding: 15 + topPadding: 10 + bottomPadding: 10 + + background: Rectangle { + id: rect + radius: 8 + border.color: UIStyle.buttonOutline + border.width: 1 + color: UIStyle.buttonBackground + } + + icon.width: 24 + icon.height: 24 + icon.color: UIStyle.textColor + + contentItem: IconLabel { + id: label + spacing: control.spacing + mirrored: control.mirrored + display: control.display + + icon: control.icon + text: control.text + font.pixelSize: UIStyle.fontSizeS + color: UIStyle.textColor + } +} diff --git a/src/inventory_wars/gui/Style/CMakeLists.txt b/src/inventory_wars/gui/Style/CMakeLists.txt new file mode 100644 index 0000000..a911f87 --- /dev/null +++ b/src/inventory_wars/gui/Style/CMakeLists.txt @@ -0,0 +1,54 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(qtexamplestyle LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/quickcontrols/colorpaletteclient/QtExampleStyle") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick QuickControls2) + +set_source_files_properties(UIStyle.qml + PROPERTIES + QT_QML_SINGLETON_TYPE TRUE +) + +qt_policy(SET QTP0001 NEW) +qt_add_qml_module(qtexamplestyle + URI QtExampleStyle + PLUGIN_TARGET qtexamplestyle + QML_FILES + Button.qml + Popup.qml + UIStyle.qml + TextField.qml +) + +target_link_libraries(qtexamplestyle PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::QuickControls2 +) + +if(UNIX AND NOT APPLE AND CMAKE_CROSSCOMPILING) + find_package(Qt6 REQUIRED COMPONENTS QuickTemplates2) + + # Work around QTBUG-86533 + target_link_libraries(qtexamplestyle PRIVATE Qt6::QuickTemplates2) +endif() + +install(TARGETS qtexamplestyle + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qmldir + DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/src/inventory_wars/gui/Style/Label.qml b/src/inventory_wars/gui/Style/Label.qml new file mode 100644 index 0000000..137af05 --- /dev/null +++ b/src/inventory_wars/gui/Style/Label.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Templates as T + +T.Label { + id: control + + font.pixelSize: UIStyle.fontSizeS + + color: UIStyle.textColor +} diff --git a/src/inventory_wars/gui/Style/Popup.qml b/src/inventory_wars/gui/Style/Popup.qml new file mode 100644 index 0000000..feffda9 --- /dev/null +++ b/src/inventory_wars/gui/Style/Popup.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Templates as T + +T.Popup { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: 15 + rightPadding: 15 + topPadding: 10 + bottomPadding: 10 + + background: Rectangle { + id: bg + radius: 8 + border.color: UIStyle.buttonOutline + border.width: 2 + color: UIStyle.background + } +} diff --git a/src/inventory_wars/gui/Style/TextField.qml b/src/inventory_wars/gui/Style/TextField.qml new file mode 100644 index 0000000..5bdd267 --- /dev/null +++ b/src/inventory_wars/gui/Style/TextField.qml @@ -0,0 +1,32 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Templates as T + +T.TextField { + id: control + placeholderText: "" + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + background: Rectangle { + implicitWidth: 200 + radius: 5 + + color: control.readOnly + ? UIStyle.buttonGray + : UIStyle.background + + border.color: UIStyle.buttonOutline + } + + color: control.readOnly + ? Qt.rgba(UIStyle.textColor.r, + UIStyle.textColor.g, + UIStyle.textColor.b, + 0.6) + : UIStyle.textColor +} diff --git a/src/inventory_wars/gui/Style/ToolBar.qml b/src/inventory_wars/gui/Style/ToolBar.qml new file mode 100644 index 0000000..a10c4b0 --- /dev/null +++ b/src/inventory_wars/gui/Style/ToolBar.qml @@ -0,0 +1,30 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Templates as T + +T.ToolBar { + id: control + + implicitHeight: 25 + spacing: 8 + + background: Rectangle { + color: UIStyle.buttonBackground + Rectangle { + height: 1 + width: parent.width + anchors.top: parent.top + anchors.left: parent.left + color: UIStyle.buttonOutline + } + Rectangle { + height: 1 + width: parent.width + anchors.bottom: parent.bottom + anchors.left: parent.left + color: UIStyle.buttonOutline + } + } +} diff --git a/src/inventory_wars/gui/Style/ToolButton.qml b/src/inventory_wars/gui/Style/ToolButton.qml new file mode 100644 index 0000000..262ef5a --- /dev/null +++ b/src/inventory_wars/gui/Style/ToolButton.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T + +T.ToolButton { + id: control + + property alias buttonColor: rect.color + property alias buttonBorderColor: rect.border.color + property alias textColor: label.color + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: 4 + rightPadding: 4 + topPadding: 4 + bottomPadding: 4 + + background: Rectangle { + id: rect + color: "transparent" + border.width: 1 + radius: 3 + border.color: control.hovered + ? UIStyle.buttonOutline + : "transparent" + } + + icon.width: 15 + icon.height: 15 + icon.color: UIStyle.textColor + + contentItem: IconLabel { + id: label + spacing: control.spacing + mirrored: control.mirrored + display: control.display + + icon: control.icon + text: control.text + font.pixelSize: UIStyle.fontSizeS + color: UIStyle.textColor + } +} diff --git a/src/inventory_wars/gui/Style/UIStyle.qml b/src/inventory_wars/gui/Style/UIStyle.qml new file mode 100644 index 0000000..424a8ec --- /dev/null +++ b/src/inventory_wars/gui/Style/UIStyle.qml @@ -0,0 +1,51 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma Singleton + +import QtQuick + +QtObject { + id: uiStyle + + property bool darkMode: (Application.styleHints.colorScheme === Qt.ColorScheme.Dark) + + // Font Sizes + readonly property int fontSizeXXS: 8 + readonly property int fontSizeXS: 10 + readonly property int fontSizeS: 12 + readonly property int fontSizeM: 16 + readonly property int fontSizeL: 20 + readonly property int fontSizeXL: 24 + + // Color Scheme + readonly property color colorRed: "#E91E63" + + readonly property color buttonGray: darkMode ? "#808080" : "#f3f3f4" + readonly property color buttonGrayPressed: darkMode ? "#707070" : "#cecfd5" + readonly property color buttonGrayOutline: darkMode ? "#0D0D0D" : "#999999" + + readonly property color buttonBackground: darkMode ? "#262626" : "#CCCCCC" + readonly property color buttonPressed: darkMode ? "#1E1E1E" : "#BEBEC4" + readonly property color buttonOutline: darkMode ? "#0D0D0D" : "#999999" + + readonly property color background: darkMode ? "#262626" : "#E6E6E6" + readonly property color background1: darkMode ? "#00414A" : "#ceded6" + + readonly property color textOnLightBackground: "#191919" + readonly property color textOnDarkBackground: "#E6E6E6" + + readonly property color textColor: darkMode ? "#E6E6E6" : "#191919" + readonly property color titletextColor: darkMode ? "#2CDE85" : "#191919" + + readonly property color highlightColor: darkMode ? "#33676E" : "#28C878" + readonly property color highlightBorderColor: darkMode ? "#4F8C95" : "#1FA05E" + + function iconPath(baseImagePath) { + if (darkMode) + return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}_dark.svg` + else + return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg` + + } +} diff --git a/src/inventory_wars/gui/Style/qmldir b/src/inventory_wars/gui/Style/qmldir new file mode 100644 index 0000000..50cd9ed --- /dev/null +++ b/src/inventory_wars/gui/Style/qmldir @@ -0,0 +1,5 @@ +module Style +Button 1.0 Button.qml +Popup 1.0 Popup.qml +TextField 1.0 TextField.qml +singleton UIStyle 1.0 UIStyle.qml diff --git a/src/inventory_wars/gui/app.py b/src/inventory_wars/gui/app.py new file mode 100644 index 0000000..d671d58 --- /dev/null +++ b/src/inventory_wars/gui/app.py @@ -0,0 +1,107 @@ +import logging +import sys +from datetime import datetime +from enum import Enum + +from PySide6.QtCore import QObject, Signal, Slot, Property, QEnum, QAbstractItemModel +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine, QmlElement +from sqlalchemy import create_engine +from sqlalchemy.orm import Session +from star_resonance_tracer.proto.enum_chit_chat_channel_type_pb2 import ChitChatChannelType + +from inventory_wars.game import Game +from inventory_wars.models import Base +from inventory_wars.scoring import Scoring, FirstGuess, FirstThenHighest +from inventory_wars.state import GameOngoing, GameIdle + +QML_IMPORT_NAME = "InventoryWars" +QML_IMPORT_MAJOR_VERSION = 1 + +engine = create_engine("sqlite:///app.db") +Base.metadata.create_all(engine) + +session = Session(engine) + + +class GameScoring(Enum): + FirstGuess = 0 + FirstGuessThenHighestAmount = 1 + + @property + def as_scoring(self) -> type[Scoring]: + match self: + case self.FirstGuess: + return FirstGuess + case self.FirstGuessThenHighestAmount: + return FirstThenHighest + case _: + raise NotImplementedError + +class GameState(Enum): + Hidden = 0 + Revealed = 1 + Ended = 3 + +@QmlElement +class GameService(QObject): + QEnum(GameScoring) + QEnum(GameState) + + stateChanged = Signal() + guessed = Signal(str, float, arguments=["username", "seconds"]) + highest = Signal(str, int, arguments=["username", "count"]) + + def __init__(self, parent=None): + super().__init__(parent) + + self.m_game = Game(session, listening_channels=[ChitChatChannelType.ChannelTeam]) + self.m_started_at: datetime | None = None + self.m_revealed_at: datetime | None = None + + @Slot(int, int) + def start(self, item_id: int, mode: int): + mode = GameScoring(mode) + self.m_game.start(item_id, mode.as_scoring()) + self.m_started_at = datetime.now() + self.m_revealed_at = None + self.stateChanged.emit() + + @Slot() + def end(self): + self.m_game.end() + self.stateChanged.emit() + + @Property(GameState, notify=stateChanged) + def state(self) -> GameState: + match self.m_game.state: + case GameIdle(): + return GameState.Ended + case GameOngoing(): + if self.m_revealed_at is None: + return GameState.Hidden + return GameState.Revealed + case _: + raise NotImplementedError + + @Property(float) + def elapsedSeconds(self) -> float | None: + if self.m_started_at: + return (datetime.now() - self.m_started_at).total_seconds() + return 0 + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + app = QGuiApplication(sys.argv) + + engine = QQmlApplicationEngine() + engine.addImportPath(sys.path[1]) + engine.loadFromModule("InventoryWars", "Main") + if not engine.rootObjects(): + sys.exit(-1) + + exit_code = app.exec() + del engine + sys.exit(exit_code)