Compare commits

..

10 Commits

100 changed files with 31545 additions and 1636 deletions

18
activate_with_env.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Helper script to activate venv and set MariaDB environment variables
# Activate virtual environment
source .venv/bin/activate
# Find and set MARIADB_CONFIG
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
if [ -n "$MARIADB_CONFIG_PATH" ]; then
export MARIADB_CONFIG="$MARIADB_CONFIG_PATH"
echo "✓ MARIADB_CONFIG set to: $MARIADB_CONFIG_PATH"
else
echo "⚠ Warning: mariadb_config not found. Install with: brew install mariadb-connector-c"
fi
echo "Virtual environment activated and ready to use!"

53
api.py
View File

@@ -1,53 +0,0 @@
from flask import Flask, redirect, url_for, render_template, session, request, copy_current_request_context, jsonify
from datetime import timedelta
import os
from basegame import *
import flask
app = flask.Flask(__name__)
app.config["DEBUG"] = True
app.secret_key = "hi"
playerNameList = []
@app.route('/', methods=['GET'])
def home():
return "<a href=\"login\">Login</a>"
@app.route('/playerlist', methods=['GET'])
def playerlist():
fuckyou = json.dumps(playerNameList)
print(fuckyou)
return fuckyou
@app.route("/login", methods=["POST", "GET"])
def login():
if request.method == "POST":
session.permanent = True # <--- makes the permanent session
user = request.form["nm"]
session["name"] = user
playerNameList.append(user)
return redirect(url_for("playerlist"))
else:
if "name" in session:
return redirect(url_for("playerlist"))
return render_template("login.html")
@app.route("/user")
def user():
if "user" in session:
user = session["user"]
return f"<h1>{user}</h1>"
else:
return redirect(url_for("login"))
@app.route("/logout")
def logout():
session.pop("user", None)
return redirect(url_for("login"))
app.run(debug=True, host='0.0.0.0')

File diff suppressed because it is too large Load Diff

380
cards.py Normal file
View File

@@ -0,0 +1,380 @@
def _coerce_int(val, default=0):
if val is None:
return default
if isinstance(val, bool):
return int(val)
try:
return int(val)
except (TypeError, ValueError):
return default
class Card:
def __init__(self):
self.name = ""
self.is_visible = False
self.is_accessible = False
def to_dict(self):
return {
"name": self.name,
"is_visible": self.is_visible,
"is_accessible": self.is_accessible,
}
def toggle_visibility(self, toggle: bool = True):
self.is_visible = toggle
def toggle_accessibility(self, toggle: bool = True):
self.is_accessible = toggle
class Starter(Card):
def __init__(self, starter_id, name, roll_match1, roll_match2, gold_payout_on_turn, gold_payout_off_turn,
strength_payout_on_turn, strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn,
has_special_payout_on_turn, has_special_payout_off_turn, special_payout_on_turn,
special_payout_off_turn, expansion):
super().__init__()
self.starter_id = starter_id
self.name = name
self.roll_match1 = roll_match1
self.roll_match2 = roll_match2
self.gold_payout_on_turn = gold_payout_on_turn
self.gold_payout_off_turn = gold_payout_off_turn
self.strength_payout_on_turn = strength_payout_on_turn
self.strength_payout_off_turn = strength_payout_off_turn
self.magic_payout_on_turn = magic_payout_on_turn
self.magic_payout_off_turn = magic_payout_off_turn
self.has_special_payout_on_turn = has_special_payout_on_turn
self.has_special_payout_off_turn = has_special_payout_off_turn
self.special_payout_on_turn = special_payout_on_turn
self.special_payout_off_turn = special_payout_off_turn
self.expansion = expansion
def to_dict(self):
return {
"starter_id": self.starter_id,
"name": self.name,
"roll_match1": self.roll_match1,
"roll_match2": self.roll_match2,
"gold_payout_on_turn": self.gold_payout_on_turn,
"gold_payout_off_turn": self.gold_payout_off_turn,
"strength_payout_on_turn": self.strength_payout_on_turn,
"strength_payout_off_turn": self.strength_payout_off_turn,
"magic_payout_on_turn": self.magic_payout_on_turn,
"magic_payout_off_turn": self.magic_payout_off_turn,
"has_special_payout_on_turn": self.has_special_payout_on_turn,
"has_special_payout_off_turn": self.has_special_payout_off_turn,
"special_payout_on_turn": self.special_payout_on_turn,
"special_payout_off_turn": self.special_payout_off_turn,
"expansion": self.expansion
}
@classmethod
def from_dict(cls, data):
return cls(data["starter_id"], data["name"], data["roll_match1"], data["roll_match2"],
data["gold_payout_on_turn"], data["gold_payout_off_turn"], data["strength_payout_on_turn"],
data["strength_payout_off_turn"], data["magic_payout_on_turn"], data["magic_payout_off_turn"],
data["has_special_payout_on_turn"], data["has_special_payout_off_turn"],
data["special_payout_on_turn"],
data["special_payout_off_turn"], data["expansion"])
class Citizen(Card):
def __init__(self, citizen_id, name, gold_cost, roll_match1, roll_match2, shadow_count, holy_count, soldier_count,
worker_count, gold_payout_on_turn, gold_payout_off_turn, strength_payout_on_turn,
strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn, has_special_payout_on_turn,
has_special_payout_off_turn, special_payout_on_turn, special_payout_off_turn, special_citizen,
expansion, is_flipped=False):
super().__init__()
self.citizen_id = citizen_id
self.name = name
self.gold_cost = gold_cost
self.roll_match1 = roll_match1
self.roll_match2 = roll_match2
self.shadow_count = _coerce_int(shadow_count)
self.holy_count = _coerce_int(holy_count)
self.soldier_count = _coerce_int(soldier_count)
self.worker_count = _coerce_int(worker_count)
self.gold_payout_on_turn = gold_payout_on_turn
self.gold_payout_off_turn = gold_payout_off_turn
self.strength_payout_on_turn = strength_payout_on_turn
self.strength_payout_off_turn = strength_payout_off_turn
self.magic_payout_on_turn = magic_payout_on_turn
self.magic_payout_off_turn = magic_payout_off_turn
self.has_special_payout_on_turn = has_special_payout_on_turn
self.has_special_payout_off_turn = has_special_payout_off_turn
self.special_payout_on_turn = special_payout_on_turn
self.special_payout_off_turn = special_payout_off_turn
self.special_citizen = special_citizen
self.expansion = expansion
self.is_flipped = bool(is_flipped)
def get_special_payout_on_turn(self):
return self.special_payout_on_turn
def to_dict(self):
base_dict = super().to_dict()
return {**base_dict,
"is_flipped": bool(getattr(self, "is_flipped", False)),
"citizen_id": self.citizen_id,
"gold_cost": self.gold_cost,
"roll_match1": self.roll_match1,
"roll_match2": self.roll_match2,
"shadow_count": self.shadow_count,
"holy_count": self.holy_count,
"soldier_count": self.soldier_count,
"worker_count": self.worker_count,
"roles": {
"shadow": self.shadow_count,
"holy": self.holy_count,
"soldier": self.soldier_count,
"worker": self.worker_count,
},
"gold_payout_on_turn": self.gold_payout_on_turn,
"gold_payout_off_turn": self.gold_payout_off_turn,
"strength_payout_on_turn": self.strength_payout_on_turn,
"strength_payout_off_turn": self.strength_payout_off_turn,
"magic_payout_on_turn": self.magic_payout_on_turn,
"magic_payout_off_turn": self.magic_payout_off_turn,
"has_special_payout_on_turn": self.has_special_payout_on_turn,
"has_special_payout_off_turn": self.has_special_payout_off_turn,
"special_payout_on_turn": self.special_payout_on_turn,
"special_payout_off_turn": self.special_payout_off_turn,
"special_citizen": self.special_citizen,
"expansion": self.expansion}
@classmethod
def from_dict(cls, dict_):
return cls(citizen_id=dict_["citizen_id"],
name=dict_["name"],
gold_cost=dict_["gold_cost"],
roll_match1=dict_["roll_match1"],
roll_match2=dict_["roll_match2"],
shadow_count=dict_.get("shadow_count"),
holy_count=dict_.get("holy_count"),
soldier_count=dict_.get("soldier_count"),
worker_count=dict_.get("worker_count"),
gold_payout_on_turn=dict_["gold_payout_on_turn"],
gold_payout_off_turn=dict_["gold_payout_off_turn"],
strength_payout_on_turn=dict_["strength_payout_on_turn"],
strength_payout_off_turn=dict_["strength_payout_off_turn"],
magic_payout_on_turn=dict_["magic_payout_on_turn"],
magic_payout_off_turn=dict_["magic_payout_off_turn"],
has_special_payout_on_turn=dict_["has_special_payout_on_turn"],
has_special_payout_off_turn=dict_["has_special_payout_off_turn"],
special_payout_on_turn=dict_["special_payout_on_turn"],
special_payout_off_turn=dict_["special_payout_off_turn"],
special_citizen=dict_["special_citizen"],
expansion=dict_["expansion"],
is_flipped=bool(dict_.get("is_flipped", False)))
class Domain(Card):
def __init__(self, domain_id, name, gold_cost, shadow_count, holy_count, soldier_count, worker_count, vp_reward,
has_activation_effect, has_passive_effect, passive_effect, activation_effect, text, expansion,
acquired_turn_number=None):
super().__init__()
self.domain_id = domain_id
self.name = name
self.gold_cost = gold_cost
self.shadow_count = _coerce_int(shadow_count)
self.holy_count = _coerce_int(holy_count)
self.soldier_count = _coerce_int(soldier_count)
self.worker_count = _coerce_int(worker_count)
self.vp_reward = vp_reward
self.has_activation_effect = has_activation_effect
self.has_passive_effect = has_passive_effect
self.passive_effect = passive_effect
self.activation_effect = activation_effect
self.text = text
self.expansion = expansion
self.acquired_turn_number = acquired_turn_number
def to_dict(self):
return {
**super().to_dict(),
"domain_id": self.domain_id,
"name": self.name,
"gold_cost": self.gold_cost,
"shadow_count": self.shadow_count,
"holy_count": self.holy_count,
"soldier_count": self.soldier_count,
"worker_count": self.worker_count,
"roles": {
"shadow": self.shadow_count,
"holy": self.holy_count,
"soldier": self.soldier_count,
"worker": self.worker_count,
},
"vp_reward": self.vp_reward,
"has_activation_effect": self.has_activation_effect,
"has_passive_effect": self.has_passive_effect,
"passive_effect": self.passive_effect,
"activation_effect": self.activation_effect,
"text": self.text,
"expansion": self.expansion,
"acquired_turn_number": getattr(self, "acquired_turn_number", None),
}
@classmethod
def from_dict(cls, dict_):
return cls(
domain_id=dict_['domain_id'],
name=dict_['name'],
gold_cost=dict_['gold_cost'],
shadow_count=dict_.get('shadow_count'),
holy_count=dict_.get('holy_count'),
soldier_count=dict_.get('soldier_count'),
worker_count=dict_.get('worker_count'),
vp_reward=dict_['vp_reward'],
has_activation_effect=dict_['has_activation_effect'],
has_passive_effect=dict_['has_passive_effect'],
passive_effect=dict_['passive_effect'],
activation_effect=dict_['activation_effect'],
text=dict_['text'],
expansion=dict_['expansion'],
acquired_turn_number=dict_.get('acquired_turn_number'),
)
class Monster(Card):
def __init__(self, monster_id, name, area, monster_type, order, strength_cost, magic_cost, vp_reward, gold_reward,
strength_reward, magic_reward, has_special_reward, special_reward, has_special_cost, special_cost,
is_extra, expansion):
super().__init__()
self.monster_id = monster_id
self.name = name
self.area = area
self.monster_type = monster_type
self.order = order
self.strength_cost = strength_cost
self.magic_cost = magic_cost
self.vp_reward = vp_reward
self.gold_reward = gold_reward
self.strength_reward = strength_reward
self.magic_reward = magic_reward
self.has_special_reward = has_special_reward
self.special_reward = special_reward
self.has_special_cost = has_special_cost
self.special_cost = special_cost
self.is_extra = is_extra
self.expansion = expansion
def to_dict(self):
card_dict = super().to_dict()
monster_dict = {
"monster_id": self.monster_id,
"area": self.area,
"monster_type": self.monster_type,
"order": self.order,
"strength_cost": self.strength_cost,
"magic_cost": self.magic_cost,
"vp_reward": self.vp_reward,
"gold_reward": self.gold_reward,
"strength_reward": self.strength_reward,
"magic_reward": self.magic_reward,
"has_special_reward": self.has_special_reward,
"special_reward": self.special_reward,
"has_special_cost": self.has_special_cost,
"special_cost": self.special_cost,
"is_extra": self.is_extra,
"expansion": self.expansion,
}
return {**card_dict, **monster_dict}
@classmethod
def from_dict(cls, d):
return cls(
d['monster_id'],
d['name'],
d['area'],
d['monster_type'],
d['order'],
d['strength_cost'],
d['magic_cost'],
d['vp_reward'],
d['gold_reward'],
d['strength_reward'],
d['magic_reward'],
d['has_special_reward'],
d['special_reward'],
d['has_special_cost'],
d['special_cost'],
d['is_extra'],
d['expansion'],
)
def add_strength_cost(self, added_strength):
self.strength_cost = self.strength_cost + added_strength
def add_magic_cost(self, added_magic):
self.magic_cost = self.magic_cost + added_magic
class Duke(Card):
def __init__(self, duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult, titan_mult,
expansion):
super().__init__()
self.duke_id = duke_id
self.name = name
self.gold_multiplier = gold_mult
self.strength_multiplier = strength_mult
self.magic_multiplier = magic_mult
self.shadow_multiplier = shadow_mult
self.holy_multiplier = holy_mult
self.soldier_multiplier = soldier_mult
self.worker_multiplier = worker_mult
self.monster_multiplier = monster_mult
self.citizen_multiplier = citizen_mult
self.domain_multiplier = domain_mult
self.boss_multiplier = boss_mult
self.minion_multiplier = minion_mult
self.beast_multiplier = beast_mult
self.titan_multiplier = titan_mult
self.expansion = expansion
def to_dict(self):
return {
**super().to_dict(),
"duke_id": self.duke_id,
"gold_multiplier": self.gold_multiplier,
"strength_multiplier": self.strength_multiplier,
"magic_multiplier": self.magic_multiplier,
"shadow_multiplier": self.shadow_multiplier,
"holy_multiplier": self.holy_multiplier,
"soldier_multiplier": self.soldier_multiplier,
"worker_multiplier": self.worker_multiplier,
"monster_multiplier": self.monster_multiplier,
"citizen_multiplier": self.citizen_multiplier,
"domain_multiplier": self.domain_multiplier,
"boss_multiplier": self.boss_multiplier,
"minion_multiplier": self.minion_multiplier,
"beast_multiplier": self.beast_multiplier,
"titan_multiplier": self.titan_multiplier,
"expansion": self.expansion
}
@classmethod
def from_dict(cls, data):
duke_id = data["duke_id"]
name = data["name"]
gold_mult = data["gold_multiplier"]
strength_mult = data["strength_multiplier"]
magic_mult = data["magic_multiplier"]
shadow_mult = data["shadow_multiplier"]
holy_mult = data["holy_multiplier"]
soldier_mult = data["soldier_multiplier"]
worker_mult = data["worker_multiplier"]
monster_mult = data["monster_multiplier"]
citizen_mult = data["citizen_multiplier"]
domain_mult = data["domain_multiplier"]
boss_mult = data["boss_multiplier"]
minion_mult = data["minion_multiplier"]
beast_mult = data["beast_multiplier"]
titan_mult = data["titan_multiplier"]
expansion = data["expansion"]
return cls(duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult,
titan_mult, expansion)

40
check_db_server.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
Simple script to check if MariaDB/MySQL server is running
Doesn't require mariadb module - just checks if port is open
"""
import socket
import sys
def check_database_server():
"""Check if database server is listening on port 3306"""
print("Checking if MariaDB/MySQL server is accessible on localhost:3306...")
print("=" * 50)
print("(Make sure SSH port forwarding is active: ssh -L 3306:localhost:3306 lukesau.com)")
host = '127.0.0.1'
port = 3306
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((host, port))
sock.close()
if result == 0:
print(f"✓ Database server is accessible at {host}:{port}")
return True
else:
print(f"✗ Cannot reach {host}:{port}")
print("\nMake sure SSH port forwarding is active:")
print(" ssh -L 3306:localhost:3306 lukesau.com")
return False
except Exception as e:
print(f"✗ Error checking server: {e}")
return False
if __name__ == "__main__":
success = check_database_server()
sys.exit(0 if success else 1)

337
client.py
View File

@@ -1,337 +0,0 @@
import wx
import socket
from common import *
class ClientVCKO(wx.App):
def OnInit(self):
self.connection_status = False
self.player_id = ""
self.player_name = ""
self.lobby = []
self.in_lobby = False
self.in_game = False
self.game_id = ""
self.game = None
self.debug_frame = DebugFrame(self)
self.lobby_frame = LobbyFrame(self)
self.game_frame = GameFrame(self)
self.last_lobby_state = ""
self.last_game_state = ""
self.debug_frame.set_connection_status()
return True
def parse_response(self, response):
if len(response) > 1000:
print(f"{response[:1000]}...")
else:
print(response)
first_word = response.split()[0]
full_command = response.split()
match first_word:
case "lobby":
if full_command[1] == "joined" and len(full_command) == 3:
self.player_id = full_command[2]
self.in_lobby = True
elif full_command[1] == "state":
json_response = ' '.join(full_command[2:])
new_lobby_state = json.loads(json_response)
if new_lobby_state != self.lobby:
self.lobby = new_lobby_state
self.lobby_frame.get_lobby_status()
else:
print("Couldn't understand that response")
case "game":
if full_command[1] == "joined" and len(full_command) == 3:
self.game_id = full_command[2]
self.in_game = True
self.in_lobby = False
self.lobby_frame.enter_game()
elif full_command[1] == "state":
json_response = ' '.join(full_command[2:])
new_game_state = json.loads(json_response)
if new_game_state == self.last_game_state:
return
self.last_game_state = new_game_state
def update_lobby_status(self):
return self.in_lobby
class GameFrame(wx.Frame):
def __init__(self, app):
super().__init__(parent=None, title='VCK Online', size=Constants.large_window_size)
self.app = app
self.panel = wx.Panel(self)
# Create a static box sizer with padding
vbox = wx.StaticBoxSizer(wx.StaticBox(self.panel, label=""), wx.VERTICAL)
vbox.AddSpacer(10) # Add a bit of padding at the top
# Wrap the list control widget inside a scrolled window
sw = wx.ScrolledWindow(vbox.GetStaticBox(), style=wx.VSCROLL)
sw.SetScrollbars(1, 1, 1, 1) # Show the scrollbars
self.game_state_list = wx.ListCtrl(sw, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
sw.SetSizer(wx.BoxSizer(wx.VERTICAL))
sw.GetSizer().Add(self.game_state_list, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)
vbox.Add(sw, proportion=1, flag=wx.EXPAND | wx.ALL, border=10) # Add the scrolled window to the sizer
vbox.AddSpacer(10) # Add a bit of padding at the bottom
# Set the sizer for the panel
self.panel.SetSizer(vbox)
self.SetMinSize(Constants.medium_window_size)
self.last_game_state = ""
self.timer_interval = 500
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.get_game_status, self.timer)
self.timer.Start(self.timer_interval)
def get_game_status(self, event=None):
if self.app.in_game and connection_check():
self.app.parse_response(send(f"game get_status {self.app.game_id}"))
if self.last_game_state == self.app.last_game_state:
if self.timer_interval < 9500:
self.timer_interval += 500
self.timer.Start(self.timer_interval)
# If the current game state is the same as the last one, don't update the list control
return
pretty_json_str = json.dumps(self.app.last_game_state, indent=4, sort_keys=False)
self.game_state_list.ClearAll()
self.game_state_list.InsertColumn(0, "Game State")
for idx, state in enumerate(pretty_json_str.split('\n')):
self.game_state_list.InsertItem(idx, state.strip())
self.game_state_list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
# Save the new game state
self.last_game_state = self.app.last_game_state
class LobbyFrame(wx.Frame):
def __init__(self, app):
super().__init__(parent=None, title='VCK Online Lobby', size=Constants.medium_window_size)
self.app = app
self.timer_interval = 500
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.get_lobby_status, self.timer)
self.timer.Start(self.timer_interval)
self.panel = wx.Panel(self)
self.vertical_sizer = wx.BoxSizer(wx.VERTICAL)
splitter = wx.SplitterWindow(self.panel)
left_panel = wx.Panel(splitter)
left_sizer = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(left_panel, label='Enter name:')
self.name_field = wx.TextCtrl(left_panel, style=wx.TE_PROCESS_ENTER, value='')
submit_button = wx.Button(left_panel, label='Submit')
submit_button.Bind(wx.EVT_BUTTON, self.on_submit)
self.name_field.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter)
left_sizer.Add(text, 0, wx.ALL, 5)
left_sizer.Add(self.name_field, 0, wx.EXPAND | wx.ALL, 5)
left_sizer.Add(submit_button, 0, wx.ALL | wx.CENTER, 5)
left_panel.SetSizer(left_sizer)
self.last_lobby_state = []
self.current_player_index = None
# Create the list control and columns
right_panel = wx.Panel(splitter)
self.list_ctrl = wx.ListCtrl(right_panel, style=wx.LC_REPORT)
self.list_ctrl.InsertColumn(0, "Player Name")
self.list_ctrl.InsertColumn(1, "Ready Status", format=wx.LIST_FORMAT_RIGHT)
self.get_lobby_status()
# Create the ready button
ready_button = wx.Button(right_panel, label="Ready Up")
ready_button.Bind(wx.EVT_BUTTON, self.on_ready_up)
self.list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.highlight_current_player)
# Add the list control and ready button to the vertical sizer
right_sizer = wx.BoxSizer(wx.VERTICAL)
right_sizer.Add(self.list_ctrl, 1, wx.ALL | wx.EXPAND, 5)
right_sizer.Add(ready_button, 0, wx.ALL | wx.CENTER, 5)
right_panel.SetSizer(right_sizer)
splitter.SplitVertically(left_panel, right_panel)
splitter.SetMinimumPaneSize(250)
splitter.SetSashGravity(0.0)
self.vertical_sizer.Add(splitter, 1, wx.EXPAND)
self.panel.SetSizer(self.vertical_sizer)
self.SetMinSize(Constants.small_window_size)
# Bind the size event to adjust the column widths
self.Bind(wx.EVT_SIZE, self.on_size)
self.Bind(wx.EVT_CLOSE, self.on_close)
self.timer_interval = 500
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.get_lobby_status, self.timer)
self.timer.Start(self.timer_interval)
self.Show()
def on_size(self, event):
# Calculate the width of each column based on the width of the list control
width = self.list_ctrl.GetSize()[0]
col_width = width // 2
self.list_ctrl.SetColumnWidth(0, col_width)
self.list_ctrl.SetColumnWidth(1, col_width)
event.Skip()
def on_submit(self, event):
name = self.name_field.GetValue()
if not name:
print("You didn't enter anything!")
else:
# Check if the player has already joined the lobby
player_exists = False
for player in self.last_lobby_state:
if player['player_id'] == self.app.player_id:
player_exists = True
break
if player_exists:
# If the player already exists, rename them
self.app.parse_response(send(f"lobby rename {self.app.player_id} {name}"))
else:
# If the player doesn't exist, join the lobby
self.app.parse_response(send(f"lobby join {name}"))
self.name_field.SetValue("")
def on_text_enter(self, event):
self.on_submit(event)
def api_call(self, message):
if connection_check():
self.app.parse_response(send(message))
self.name_field.SetValue("")
def get_lobby_status(self, event=None):
if connection_check():
self.app.parse_response(send(f"lobby get_status {self.app.player_id}"))
if self.app.lobby == self.last_lobby_state:
# If the current lobby state is the same as the last one, don't update the list control
if self.timer_interval < 9500:
self.timer_interval += 500
self.timer.Start(self.timer_interval)
return
self.list_ctrl.DeleteAllItems()
for index, player in enumerate(self.app.lobby):
self.list_ctrl.InsertItem(index, player['name'])
self.list_ctrl.SetItem(index, 1, "Ready" if player['is_ready'] else "Not Ready")
if player['player_id'] == self.app.player_id:
self.current_player_index = index
self.list_ctrl.SetItemState(index, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)
else:
self.list_ctrl.SetItemState(index, 0, wx.LIST_STATE_SELECTED)
# Save the new lobby state
self.last_lobby_state = self.app.lobby
def highlight_current_player(self, event=None):
if self.current_player_index is not None:
self.list_ctrl.Select(self.current_player_index)
else:
self.list_ctrl.Select(-1)
def on_ready_up(self, event):
for player in self.app.lobby:
if player['player_id'] == self.app.player_id:
if player['is_ready']:
if connection_check():
self.app.parse_response(send(f"lobby unready {self.app.player_id}"))
else:
if connection_check():
self.app.parse_response(send(f"lobby ready {self.app.player_id}"))
break
def enter_game(self, event=None):
self.app.game_frame.Show()
self.Hide()
def on_close(self, event):
self.app.parse_response(send(f"lobby leave {self.app.player_id}"))
self.Destroy()
class DebugFrame(wx.Frame):
def __init__(self, app):
super().__init__(parent=None, title='VCKO Debug Console', size=Constants.small_window_size)
self.app = app
self.panel = wx.Panel(self)
self.vertical_sizer = wx.BoxSizer(wx.VERTICAL)
self.status_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.message_field = wx.TextCtrl(self.panel, style=wx.TE_PROCESS_ENTER)
self.connection_status_indicator = wx.StaticText(self.panel, label="Connection Status")
self.my_btn = wx.Button(self.panel, label="Send call")
self.my_btn.Bind(wx.EVT_BUTTON, self.on_press)
self.message_field.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter)
# Create a horizontal sizer to hold the connection_status StaticText
self.status_sizer.AddStretchSpacer()
self.status_sizer.Add(wx.StaticText(self.panel), 0, wx.EXPAND | wx.RIGHT, 5)
self.status_sizer.Add(self.connection_status_indicator, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.status_sizer.Add(wx.StaticText(self.panel), 0, wx.EXPAND | wx.LEFT, 5)
# Add the text field, button, and status sizer to the vertical sizer
self.vertical_sizer.Add(self.message_field, 0, wx.ALL | wx.EXPAND, 5)
self.vertical_sizer.Add(self.my_btn, 0, wx.ALL | wx.CENTER, 5)
self.vertical_sizer.AddStretchSpacer()
self.vertical_sizer.Add(self.status_sizer, 0, wx.ALIGN_LEFT | wx.BOTTOM, 5)
self.panel.SetSizer(self.vertical_sizer)
self.SetMinSize(Constants.small_window_size)
self.Show()
# Create a timer to call the connection_check method every 2 seconds
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.set_connection_status, self.timer)
self.timer.Start(10000)
def set_connection_status(self, event=None):
if connection_check():
self.connection_status_indicator.SetLabel("Connected")
self.connection_status_indicator.SetForegroundColour(Constants.green)
else:
self.connection_status_indicator.SetLabel("Not Connected")
self.connection_status_indicator.SetForegroundColour(Constants.red)
def on_press(self, event):
message = self.message_field.GetValue()
if not message:
print("You didn't enter anything!")
else:
self.api_call(message)
def on_text_enter(self, event):
self.on_press(event)
def api_call(self, message):
if connection_check():
self.app.parse_response(send(message))
self.message_field.SetValue("")
def connection_check():
try:
response = send("connection_check")
if response == "received":
return True
else:
return False
except ConnectionRefusedError:
return False
except BrokenPipeError:
return False
def send(message):
client_socket = socket.socket()
client_socket.connect((Constants.host, Constants.port))
message_bytes = message.encode(Constants.encoding)
send_data(client_socket, message_bytes)
response = receive_data(client_socket)
client_socket.close()
return response.decode(Constants.encoding)
if __name__ == '__main__':
the_app = ClientVCKO()
the_app.MainLoop()

824
common.py
View File

@@ -1,824 +0,0 @@
import json
import time
from json import JSONEncoder, JSONDecoder
import random
from typing import List, Dict
from constants import *
import shortuuid
import uuid
class Card:
def __init__(self):
self.name = ""
self.is_visible = False
self.is_accessible = False
def to_dict(self):
return {
"name": self.name,
"is_visible": self.is_visible,
"is_accessible": self.is_accessible,
}
def toggle_visibility(self, toggle: bool = True):
self.is_visible = toggle
def toggle_accessibility(self, toggle: bool = True):
self.is_accessible = toggle
class Player:
def __init__(self, player_id, name):
self.player_id = player_id
self.name = name
self.owned_starters = []
self.owned_citizens = []
self.owned_domains = []
self.owned_dukes = []
self.owned_monsters = []
self.gold_score = 2
self.strength_score = 0
self.magic_score = 1
self.victory_score = 0
self.is_first = False
self.shadow_count = 0
self.holy_count = 0
self.soldier_count = 0
self.worker_count = 0
self.effects = {
"roll_phase": [],
"harvest_phase": [],
"action_phase": []
}
@classmethod
def from_dict(cls, data):
player_id = data['player_id']
name = data['name']
player = cls(player_id, name)
player.owned_starters = [Starter.from_dict(s) for s in data['owned_starters']]
player.owned_citizens = [Citizen.from_dict(c) for c in data['owned_citizens']]
player.owned_domains = [Domain.from_dict(d) for d in data['owned_domains']]
player.owned_dukes = [Duke.from_dict(d) for d in data['owned_dukes']]
player.owned_monsters = [Monster.from_dict(m) for m in data['owned_monsters']]
player.gold_score = data['gold_score']
player.strength_score = data['strength_score']
player.magic_score = data['magic_score']
player.victory_score = data['victory_score']
player.is_first = data['is_first']
player.shadow_count = data['shadow_count']
player.holy_count = data['holy_count']
player.soldier_count = data['soldier_count']
player.worker_count = data['worker_count']
player.effects = data['effects']
return player
def calc_roles(self):
shadow_count = 0
holy_count = 0
soldier_count = 0
worker_count = 0
for citizen in self.owned_citizens:
shadow_count = shadow_count + citizen.shadow_count
holy_count = holy_count + citizen.holy_count
soldier_count = soldier_count + citizen.soldier_count
worker_count = worker_count + citizen.worker_count
for domain in self.owned_domains:
shadow_count = shadow_count + domain.shadow_count
holy_count = holy_count + domain.holy_count
soldier_count = soldier_count + domain.soldier_count
worker_count = worker_count + domain.worker_count
roles_dict = {
"shadow_count": shadow_count,
"holy_count": holy_count,
"soldier_count": soldier_count,
"worker_count": worker_count
}
return roles_dict
class Starter(Card):
def __init__(self, starter_id, name, roll_match1, roll_match2, gold_payout_on_turn, gold_payout_off_turn,
strength_payout_on_turn, strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn,
has_special_payout_on_turn, has_special_payout_off_turn, special_payout_on_turn,
special_payout_off_turn, expansion):
super().__init__()
self.starter_id = starter_id
self.name = name
self.roll_match1 = roll_match1
self.roll_match2 = roll_match2
self.gold_payout_on_turn = gold_payout_on_turn
self.gold_payout_off_turn = gold_payout_off_turn
self.strength_payout_on_turn = strength_payout_on_turn
self.strength_payout_off_turn = strength_payout_off_turn
self.magic_payout_on_turn = magic_payout_on_turn
self.magic_payout_off_turn = magic_payout_off_turn
self.has_special_payout_on_turn = has_special_payout_on_turn
self.has_special_payout_off_turn = has_special_payout_off_turn
self.special_payout_on_turn = special_payout_on_turn
self.special_payout_off_turn = special_payout_off_turn
self.expansion = expansion
def to_dict(self):
return {
"starter_id": self.starter_id,
"name": self.name,
"roll_match1": self.roll_match1,
"roll_match2": self.roll_match2,
"gold_payout_on_turn": self.gold_payout_on_turn,
"gold_payout_off_turn": self.gold_payout_off_turn,
"strength_payout_on_turn": self.strength_payout_on_turn,
"strength_payout_off_turn": self.strength_payout_off_turn,
"magic_payout_on_turn": self.magic_payout_on_turn,
"magic_payout_off_turn": self.magic_payout_off_turn,
"has_special_payout_on_turn": self.has_special_payout_on_turn,
"has_special_payout_off_turn": self.has_special_payout_off_turn,
"special_payout_on_turn": self.special_payout_on_turn,
"special_payout_off_turn": self.special_payout_off_turn,
"expansion": self.expansion
}
@classmethod
def from_dict(cls, data):
return cls(data["starter_id"], data["name"], data["roll_match1"], data["roll_match2"],
data["gold_payout_on_turn"], data["gold_payout_off_turn"], data["strength_payout_on_turn"],
data["strength_payout_off_turn"], data["magic_payout_on_turn"], data["magic_payout_off_turn"],
data["has_special_payout_on_turn"], data["has_special_payout_off_turn"],
data["special_payout_on_turn"],
data["special_payout_off_turn"], data["expansion"])
class Citizen(Card):
def __init__(self, citizen_id, name, gold_cost, roll_match1, roll_match2, shadow_count, holy_count, soldier_count,
worker_count, gold_payout_on_turn, gold_payout_off_turn, strength_payout_on_turn,
strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn, has_special_payout_on_turn,
has_special_payout_off_turn, special_payout_on_turn, special_payout_off_turn, special_citizen,
expansion):
super().__init__()
self.citizen_id = citizen_id
self.name = name
self.gold_cost = gold_cost
self.roll_match1 = roll_match1
self.roll_match2 = roll_match2
self.shadow_count = shadow_count
self.holy_count = holy_count
self.soldier_count = soldier_count
self.worker_count = worker_count
self.gold_payout_on_turn = gold_payout_on_turn
self.gold_payout_off_turn = gold_payout_off_turn
self.strength_payout_on_turn = strength_payout_on_turn
self.strength_payout_off_turn = strength_payout_off_turn
self.magic_payout_on_turn = magic_payout_on_turn
self.magic_payout_off_turn = magic_payout_off_turn
self.has_special_payout_on_turn = has_special_payout_on_turn
self.has_special_payout_off_turn = has_special_payout_off_turn
self.special_payout_on_turn = special_payout_on_turn
self.special_payout_off_turn = special_payout_off_turn
self.special_citizen = special_citizen
self.expansion = expansion
def get_special_payout_on_turn(self):
return self.special_payout_on_turn
def to_dict(self):
base_dict = super().to_dict()
return {**base_dict,
"citizen_id": self.citizen_id,
"gold_cost": self.gold_cost,
"roll_match1": self.roll_match1,
"roll_match2": self.roll_match2,
"shadow_count": self.shadow_count,
"holy_count": self.holy_count,
"soldier_count": self.soldier_count,
"worker_count": self.worker_count,
"gold_payout_on_turn": self.gold_payout_on_turn,
"gold_payout_off_turn": self.gold_payout_off_turn,
"strength_payout_on_turn": self.strength_payout_on_turn,
"strength_payout_off_turn": self.strength_payout_off_turn,
"magic_payout_on_turn": self.magic_payout_on_turn,
"magic_payout_off_turn": self.magic_payout_off_turn,
"has_special_payout_on_turn": self.has_special_payout_on_turn,
"has_special_payout_off_turn": self.has_special_payout_off_turn,
"special_payout_on_turn": self.special_payout_on_turn,
"special_payout_off_turn": self.special_payout_off_turn,
"special_citizen": self.special_citizen,
"expansion": self.expansion}
@classmethod
def from_dict(cls, dict_):
return cls(citizen_id=dict_["citizen_id"],
name=dict_["name"],
gold_cost=dict_["gold_cost"],
roll_match1=dict_["roll_match1"],
roll_match2=dict_["roll_match2"],
shadow_count=dict_["shadow_count"],
holy_count=dict_["holy_count"],
soldier_count=dict_["soldier_count"],
worker_count=dict_["worker_count"],
gold_payout_on_turn=dict_["gold_payout_on_turn"],
gold_payout_off_turn=dict_["gold_payout_off_turn"],
strength_payout_on_turn=dict_["strength_payout_on_turn"],
strength_payout_off_turn=dict_["strength_payout_off_turn"],
magic_payout_on_turn=dict_["magic_payout_on_turn"],
magic_payout_off_turn=dict_["magic_payout_off_turn"],
has_special_payout_on_turn=dict_["has_special_payout_on_turn"],
has_special_payout_off_turn=dict_["has_special_payout_off_turn"],
special_payout_on_turn=dict_["special_payout_on_turn"],
special_payout_off_turn=dict_["special_payout_off_turn"],
special_citizen=dict_["special_citizen"],
expansion=dict_["expansion"])
class Domain(Card):
def __init__(self, domain_id, name, gold_cost, shadow_count, holy_count, soldier_count, worker_count, vp_reward,
has_activation_effect, has_passive_effect, passive_effect, activation_effect, text, expansion):
super().__init__()
self.domain_id = domain_id
self.name = name
self.gold_cost = gold_cost
self.shadow_count = shadow_count
self.holy_count = holy_count
self.soldier_count = soldier_count
self.worker_count = worker_count
self.vp_reward = vp_reward
self.has_activation_effect = has_activation_effect
self.has_passive_effect = has_passive_effect
self.passive_effect = passive_effect
self.activation_effect = activation_effect
self.text = text
self.expansion = expansion
def to_dict(self):
return {
**super().to_dict(),
"domain_id": self.domain_id,
"name": self.name,
"gold_cost": self.gold_cost,
"shadow_count": self.shadow_count,
"holy_count": self.holy_count,
"soldier_count": self.soldier_count,
"worker_count": self.worker_count,
"vp_reward": self.vp_reward,
"has_activation_effect": self.has_activation_effect,
"has_passive_effect": self.has_passive_effect,
"passive_effect": self.passive_effect,
"activation_effect": self.activation_effect,
"text": self.text,
"expansion": self.expansion
}
@classmethod
def from_dict(cls, dict_):
return cls(
domain_id=dict_['domain_id'],
name=dict_['name'],
gold_cost=dict_['gold_cost'],
shadow_count=dict_['shadow_count'],
holy_count=dict_['holy_count'],
soldier_count=dict_['soldier_count'],
worker_count=dict_['worker_count'],
vp_reward=dict_['vp_reward'],
has_activation_effect=dict_['has_activation_effect'],
has_passive_effect=dict_['has_passive_effect'],
passive_effect=dict_['passive_effect'],
activation_effect=dict_['activation_effect'],
text=dict_['text'],
expansion=dict_['expansion']
)
class Monster(Card):
def __init__(self, monster_id, name, area, monster_type, order, strength_cost, magic_cost, vp_reward, gold_reward,
strength_reward, magic_reward, has_special_reward, special_reward, has_special_cost, special_cost,
is_extra, expansion):
super().__init__()
self.monster_id = monster_id
self.name = name
self.area = area
self.monster_type = monster_type
self.order = order
self.strength_cost = strength_cost
self.magic_cost = magic_cost
self.vp_reward = vp_reward
self.gold_reward = gold_reward
self.strength_reward = strength_reward
self.magic_reward = magic_reward
self.has_special_reward = has_special_reward
self.special_reward = special_reward
self.has_special_cost = has_special_cost
self.special_cost = special_cost
self.is_extra = is_extra
self.expansion = expansion
def to_dict(self):
card_dict = super().to_dict()
monster_dict = {
"monster_id": self.monster_id,
"area": self.area,
"monster_type": self.monster_type,
"order": self.order,
"strength_cost": self.strength_cost,
"magic_cost": self.magic_cost,
"vp_reward": self.vp_reward,
"gold_reward": self.gold_reward,
"strength_reward": self.strength_reward,
"magic_reward": self.magic_reward,
"has_special_reward": self.has_special_reward,
"special_reward": self.special_reward,
"has_special_cost": self.has_special_cost,
"special_cost": self.special_cost,
"is_extra": self.is_extra,
"expansion": self.expansion,
}
return {**card_dict, **monster_dict}
@classmethod
def from_dict(cls, d):
return cls(
d['monster_id'],
d['name'],
d['area'],
d['monster_type'],
d['order'],
d['strength_cost'],
d['magic_cost'],
d['vp_reward'],
d['gold_reward'],
d['strength_reward'],
d['magic_reward'],
d['has_special_reward'],
d['special_reward'],
d['has_special_cost'],
d['special_cost'],
d['is_extra'],
d['expansion'],
)
def add_strength_cost(self, added_strength):
self.strength_cost = self.strength_cost + added_strength
def add_magic_cost(self, added_magic):
self.magic_cost = self.magic_cost + added_magic
class Duke(Card):
def __init__(self, duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult, titan_mult,
expansion):
super().__init__()
self.duke_id = duke_id
self.name = name
self.gold_multiplier = gold_mult
self.strength_multiplier = strength_mult
self.magic_multiplier = magic_mult
self.shadow_multiplier = shadow_mult
self.holy_multiplier = holy_mult
self.soldier_multiplier = soldier_mult
self.worker_multiplier = worker_mult
self.monster_multiplier = monster_mult
self.citizen_multiplier = citizen_mult
self.domain_multiplier = domain_mult
self.boss_multiplier = boss_mult
self.minion_multiplier = minion_mult
self.beast_multiplier = beast_mult
self.titan_multiplier = titan_mult
self.expansion = expansion
def to_dict(self):
return {
**super().to_dict(),
"duke_id": self.duke_id,
"gold_multiplier": self.gold_multiplier,
"strength_multiplier": self.strength_multiplier,
"magic_multiplier": self.magic_multiplier,
"shadow_multiplier": self.shadow_multiplier,
"holy_multiplier": self.holy_multiplier,
"soldier_multiplier": self.soldier_multiplier,
"worker_multiplier": self.worker_multiplier,
"monster_multiplier": self.monster_multiplier,
"citizen_multiplier": self.citizen_multiplier,
"domain_multiplier": self.domain_multiplier,
"boss_multiplier": self.boss_multiplier,
"minion_multiplier": self.minion_multiplier,
"beast_multiplier": self.beast_multiplier,
"titan_multiplier": self.titan_multiplier,
"expansion": self.expansion
}
@classmethod
def from_dict(cls, data):
duke_id = data["duke_id"]
name = data["name"]
gold_mult = data["gold_multiplier"]
strength_mult = data["strength_multiplier"]
magic_mult = data["magic_multiplier"]
shadow_mult = data["shadow_multiplier"]
holy_mult = data["holy_multiplier"]
soldier_mult = data["soldier_multiplier"]
worker_mult = data["worker_multiplier"]
monster_mult = data["monster_multiplier"]
citizen_mult = data["citizen_multiplier"]
domain_mult = data["domain_multiplier"]
boss_mult = data["boss_multiplier"]
minion_mult = data["minion_multiplier"]
beast_mult = data["beast_multiplier"]
titan_mult = data["titan_multiplier"]
expansion = data["expansion"]
return cls(duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult,
titan_mult, expansion)
class Game:
def __init__(self, game_state):
self.game_id = game_state['game_id']
self.player_list = game_state['player_list']
self.monster_grid = game_state['monster_grid']
self.citizen_grid = game_state['citizen_grid']
self.domain_grid = game_state['domain_grid']
self.die_one = game_state['die_one']
self.die_two = game_state['die_two']
self.die_sum = game_state['die_sum']
self.exhausted_count = game_state['exhausted_count']
self.effects = game_state['effects']
self.action_required = game_state['action_required']
def roll_phase(self):
self.die_one = random.randint(1, 6)
self.die_two = random.randint(1, 6)
self.die_sum = self.die_one + self.die_two
print(f"{self.die_one} | {self.die_two} | {self.die_sum}")
# check for player effects that are able to change roll
# check for board effects that trigger from rolls
def harvest_phase(self):
# steal activates first
for starter in self.player_list[0].owned_starters:
if (starter.roll_match1 == self.die_one) or (starter.roll_match1 == self.die_two) or (
starter.roll_match1 == self.die_sum) or (starter.roll_match2 == self.die_sum):
count = 1
if starter.roll_match1 == self.die_one == self.die_two:
count = 2
print(f"Payout for {self.player_list[0].name}: Starter {starter.name}{' x2' if count == 2 else ''}")
for i in range(count):
self.player_list[0].gold_score = self.player_list[0].gold_score + starter.gold_payout_on_turn
self.player_list[0].strength_score = self.player_list[
0].strength_score + starter.strength_payout_on_turn
self.player_list[0].magic_score = self.player_list[0].magic_score + starter.magic_payout_on_turn
if starter.has_special_payout_on_turn:
payout = self.execute_special_payout(starter.special_payout_on_turn,
self.player_list[0].player_id)
self.player_list[0].gold_score = self.player_list[0].gold_score + payout[0]
self.player_list[0].strength_score = self.player_list[0].strength_score + payout[1]
self.player_list[0].magic_score = self.player_list[0].magic_score + payout[2]
for citizen in self.player_list[0].owned_citizens:
if (citizen.roll_match1 == self.die_one) or (citizen.roll_match1 == self.die_two) or (
citizen.roll_match1 == self.die_sum) or (citizen.roll_match2 == self.die_sum):
count = 1
if citizen.roll_match1 == self.die_one == self.die_two:
count = 2
print(f"Payout for {self.player_list[0].name}: Citizen {citizen.name}{' x2' if count == 2 else ''}")
for i in range(count):
self.player_list[0].gold_score = self.player_list[0].gold_score + citizen.gold_payout_on_turn
self.player_list[0].strength_score = self.player_list[
0].strength_score + citizen.strength_payout_on_turn
self.player_list[0].magic_score = self.player_list[0].magic_score + citizen.magic_payout_on_turn
if citizen.has_special_payout_on_turn:
print(f"Citizen {citizen.name} special payout text: {citizen.special_payout_on_turn}")
payout = self.execute_special_payout(citizen.special_payout_on_turn,
self.player_list[0].player_id)
print(f"right after running execute special payout {payout}")
self.player_list[0].gold_score = self.player_list[0].gold_score + payout[0]
self.player_list[0].strength_score = self.player_list[0].strength_score + payout[1]
self.player_list[0].magic_score = self.player_list[0].magic_score + payout[2]
list_iterator = iter(self.player_list) # skip first player when paying out the rest of the board
next(list_iterator)
for player in list_iterator:
for starter in player.owned_starters:
if (starter.roll_match1 == self.die_one) or (starter.roll_match1 == self.die_two) or (
starter.roll_match1 == self.die_sum) or (starter.roll_match2 == self.die_sum):
count = 1
if starter.roll_match1 == self.die_one == self.die_two:
count = 2
print(f"Payout for {player.name}: Starter {starter.name}{' x2' if count == 2 else ''}")
for i in range(count):
player.gold_score = player.gold_score + starter.gold_payout_off_turn
player.strength_score = player.strength_score + starter.strength_payout_off_turn
player.magic_score = player.magic_score + starter.magic_payout_off_turn
if starter.has_special_payout_off_turn:
payout = self.execute_special_payout(starter.special_payout_off_turn, player.player_id)
player.gold_score = player.gold_score + payout[0]
player.strength_score = player.strength_score + payout[1]
player.magic_score = player.magic_score + payout[2]
for citizen in player.owned_citizens:
if (citizen.roll_match1 == self.die_one) or (citizen.roll_match1 == self.die_two) or (
citizen.roll_match1 == self.die_sum) or (citizen.roll_match2 == self.die_sum):
count = 1
if citizen.roll_match1 == self.die_one == self.die_two:
count = 2
print(f"Payout for {player.name}: Citizen {citizen.name}{' x2' if count == 2 else ''}")
for i in range(count):
player.gold_score = player.gold_score + citizen.gold_payout_off_turn
player.strength_score = player.strength_score + citizen.strength_payout_off_turn
player.magic_score = player.magic_score + citizen.magic_payout_off_turn
if citizen.has_special_payout_off_turn:
print("special payout off turn triggered")
print(citizen.special_payout_off_turn)
payout = self.execute_special_payout(citizen.special_payout_off_turn, player.player_id)
player.gold_score = player.gold_score + payout[0]
player.strength_score = player.strength_score + payout[1]
player.magic_score = player.magic_score + payout[2]
for player in self.player_list:
print(f"Player {player.name}: {player.gold_score} G, {player.strength_score} S, {player.magic_score} M,"
f" {player.victory_score} VP, Monsters: {len(player.owned_monsters)}, "
f"Citizens: {len(player.owned_citizens)}, Domains {len(player.owned_domains)}")
def execute_special_payout(self, command, player_id):
print("executing special payout")
payout = [0, 0, 0, 0] # gp, sp, mp, vp, todo: citizen, monster, domain
split_command = command.split()
first_word = split_command[0]
second_word = split_command[1]
third_word = split_command[2]
fourth_word = split_command[3]
match first_word:
case "count":
print("Matched count")
match second_word:
case "owned_shadow":
self.update_payout_for_role('shadow_count', player_id, payout, split_command)
case "owned_holy":
self.update_payout_for_role('holy_count', player_id, payout, split_command)
case "owned_soldier":
self.update_payout_for_role('soldier_count', player_id, payout, split_command)
case "owned_worker":
self.update_payout_for_role('worker_count', player_id, payout, split_command)
case "owned_monsters":
self.update_payout_for_role('owned_monsters', player_id, payout, split_command)
case "owned_citizens":
self.update_payout_for_role('owned_citizens', player_id, payout, split_command)
case "owned_domains":
self.update_payout_for_role('owned_domains', player_id, payout, split_command)
case _:
payout[0] = -9999
case "exchange":
print("Matched exchange")
match second_word:
case 'g':
payout[0] = payout[0] - int(third_word)
case 's':
payout[1] = payout[1] - int(third_word)
case 'm':
payout[2] = payout[2] - int(third_word)
case 'v':
payout[3] = payout[3] - int(third_word)
case _:
payout[0] = -9999
match fourth_word:
case 'g':
payout[0] = payout[0] + int(split_command[4])
case 's':
payout[1] = payout[1] + int(split_command[4])
case 'm':
payout[2] = payout[2] + int(split_command[4])
case 'v':
payout[3] = payout[3] + int(split_command[4])
case _:
payout[0] = -9999
case "choose":
print("Matched choose")
self.action_required['player_id'] = player_id
self.action_required['action'] = command
# need to pause execution here until we get player input
while self.action_required['player_id'] != self.game_id:
time.sleep(1)
choice = []
match self.action_required['action']:
case 'choose 1':
choice = [second_word, third_word]
case 'choose 2':
choice = [fourth_word, split_command[4]]
case 'choose 3':
choice = [split_command[5], split_command[6]] # [sixth_word, seventh_word]
case _:
payout[0] = -9999
match choice[0]:
case 'g':
payout[0] = payout[0] + choice[1]
case 's':
payout[1] = payout[1] + choice[1]
case 'm':
payout[2] = payout[2] + choice[1]
case 'v':
payout[3] = payout[3] + choice[1]
case _:
payout[0] = -9999
case _:
payout[0] = -9999
print(payout)
return payout
def update_payout_for_role(self, role_name, player_id, payout, split_command):
role_count = 0
for player in self.player_list:
if player.player_id == player_id:
role_count = player.calc_roles()[role_name]
break
if role_count > 0:
match split_command[2]:
case 'g':
payout[0] = int(split_command[3]) * role_count
case 's':
payout[1] = int(split_command[3]) * role_count
case 'm':
payout[2] = int(split_command[3]) * role_count
case 'v':
payout[3] = int(split_command[3]) * role_count
case _:
payout[0] = -9999
else:
payout[0] = -9999
def hire_citizen(self, player_id, citizen_id, gp, mp=0):
for citizen_stack in self.citizen_grid:
for citizen in citizen_stack:
if citizen.citizen_id == citizen_id and citizen.is_accessible:
for player in self.player_list:
if player.player_id == player_id:
player.gold_score = player.gold_score - gp
player.magic_score = player.magic_score - mp
player.owned_citizens.append(citizen_stack.pop(-1))
citizen_stack[-1].toggle_accessibility(True)
def slay_monster(self, player_id, monster_id, sp, mp=0):
for monster_stack in self.monster_grid:
for monster in monster_stack:
if monster.monster_id == monster_id and monster.is_accessible:
for player in self.player_list:
if player.player_id == player_id:
player.strength_score = player.strength_score - sp
player.magic_score = player.magic_score - mp
player.owned_monsters.append(monster_stack.pop(-1))
monster_stack[-1].toggle_accessibility(True)
def buy_domain(self, player_id, domain_id, gp, mp=0):
for domain_stack in self.domain_grid:
for domain in domain_stack:
if domain.domain_id == domain_id and domain.is_accessible:
for player in self.player_list:
if player.player_id == player_id:
player.gold_score = player.gold_score - gp
player.magic_score = player.magic_score - mp
player.owned_domains.append(domain_stack.pop(-1))
domain_stack[-1].toggle_accessibility(True)
def action_phase(self):
return
def play_turn(self):
self.roll_phase()
self.harvest_phase()
self.action_phase()
def end_check(self):
if self.exhausted_count <= (len(self.player_list) * 2):
return False
def prompt(self):
return
class SummaryEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Player):
return {
'player_id': obj.player_id,
'name': obj.name,
'owned_citizens': len(obj.owned_citizens),
'owned_domains': len(obj.owned_domains),
'owned_monsters': len(obj.owned_monsters),
'gold_score': obj.gold_score,
'strength_score': obj.strength_score,
'magic_score': obj.magic_score,
'victory_score': obj.victory_score,
'is_first': obj.is_first
}
elif isinstance(obj, LobbyMember):
return {
"player_name": obj.name,
"player_id": obj.player_id,
"is_ready": obj.is_ready
}
elif isinstance(obj, GameMember):
return {
"player_name": obj.name,
"player_id": obj.player_id
}
elif isinstance(obj, Game):
return {
"game_id": obj.game_id,
"player_list": obj.player_list
}
else:
return super().default(obj)
class GameObjectEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Player):
return {
'player_id': obj.player_id,
'name': obj.name,
'owned_starters': [starter.starter_id for starter in obj.owned_starters],
'owned_citizens': [citizen.citizen_id for citizen in obj.owned_citizens],
'owned_domains': [domain.domain_id for domain in obj.owned_domains],
'owned_dukes': [duke.duke_id for duke in obj.owned_dukes],
'owned_monsters': [monster.monster_id for monster in obj.owned_monsters],
'gold_score': obj.gold_score,
'strength_score': obj.strength_score,
'magic_score': obj.magic_score,
'victory_score': obj.victory_score,
'is_first': obj.is_first,
'shadow_count': obj.shadow_count,
'holy_count': obj.holy_count,
'soldier_count': obj.soldier_count,
'worker_count': obj.worker_count,
'effects': obj.effects
}
elif isinstance(obj, Duke):
return obj.to_dict()
elif isinstance(obj, Monster):
return obj.to_dict()
elif isinstance(obj, Starter):
return obj.to_dict()
elif isinstance(obj, Citizen):
return obj.to_dict()
elif isinstance(obj, Domain):
return obj.to_dict()
elif isinstance(obj, Game):
return {
"game_id": obj.game_id,
"player_list": obj.player_list,
"monster_grid": obj.monster_grid,
"citizen_grid": obj.citizen_grid,
"domain_grid": obj.domain_grid,
"die_one": obj.die_one,
"die_two": obj.die_two,
"die_sum": obj.die_sum,
"exhausted_count": obj.exhausted_count,
"effects": obj.effects,
"action_required": obj.action_required
}
else:
return super().default(obj)
def send_data(conn, data):
header = f"{len(data):<{Constants.header_size}}"
conn.send(header.encode(Constants.encoding))
offset = 0
while offset < len(data):
chunk = data[offset:offset + Constants.buffer_size]
conn.send(chunk)
offset += Constants.buffer_size
def receive_data(conn):
# Read the header to determine the message length
header = b""
while len(header) < Constants.header_size:
chunk = conn.recv(Constants.header_size - len(header))
if not chunk:
raise ConnectionError("Connection closed by server")
header += chunk
msg_length = int(header.decode(Constants.encoding).strip())
# Read the message in chunks until the entire message is received
data = b""
while len(data) < msg_length:
chunk_size = min(Constants.buffer_size, msg_length - len(data))
chunk = conn.recv(chunk_size)
if not chunk:
raise ConnectionError("Connection closed by server")
data += chunk
return data
class LobbyMember:
def __init__(self, player_name, player_id):
self.name = player_name
self.player_id = player_id
self.is_ready = False
self.last_active_time = 0
class GameMember:
def __init__(self, player_id, player_name, game_id):
self.name = player_name
self.player_id = player_id
self.game_id = game_id

View File

@@ -2,8 +2,8 @@ class Constants:
green = (106, 171, 115)
red = (219, 92, 92)
server_host = '192.168.1.99'
host = "lukesau.com"
# host = "127.0.1.1"
# host = "lukesau.com"
host = "127.0.1.1"
port = 8328
text_format = "utf-8"
small_window_size = (300, 150)
@@ -12,3 +12,5 @@ class Constants:
header_size = 512
buffer_size = 4096
encoding = "utf-8"
areas = ['Hills', 'Ruins', 'Forest', 'Valley', 'Mountains']
types = ['Minion', 'Titan', 'Warden', 'Boss', 'Beast']

13
docs/README.md Normal file
View File

@@ -0,0 +1,13 @@
# VCK Online Docs
This folder contains developer documentation for the VCK Online dev/test server and game engine.
## Start here
- `README_SERVER.md`: how to run the FastAPI server and use the dev HTML client
- `dev-setup.md`: local environment setup (venv + MariaDB connector)
- `database.md`: database expectations, SSH tunnel, and stored procedure setup
- `server.md`: FastAPI server architecture and API surface
- `game.md`: game engine model and the DB-backed game bootstrap flow
- `testing.md`: how to run the included test scripts

79
docs/README_SERVER.md Normal file
View File

@@ -0,0 +1,79 @@
# VCK Online FastAPI Server
Simple REST API server for developing and testing the Valeria Card Kingdoms Online game.
## Setup
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Make sure database is accessible (SSH tunnel if needed):
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
## Running the Server
```bash
python3 server.py
```
Or with uvicorn directly:
```bash
uvicorn server:app --host 0.0.0.0 --port 8000 --reload
```
The server will start on `http://localhost:8000`
## API Endpoints
### Lobby
- `POST /api/lobby/join` - Join lobby with name
```json
{"name": "Player Name"}
```
Returns: `{"player_id": "...", "message": "Joined lobby"}`
- `POST /api/lobby/ready` - Mark player as ready
```json
{"player_id": "..."}
```
Returns game info if all players ready
- `POST /api/lobby/unready` - Mark player as not ready
- `POST /api/lobby/leave?player_id=...` - Leave lobby
- `GET /api/lobby/status?player_id=...` - Get lobby status
### Game
- `GET /api/game/{game_id}/state` - Get current game state
- `POST /api/game/{game_id}/action` - Perform game action
```json
{
"player_id": "...",
"action_type": "hire_citizen|build_domain|slay_monster|act_on_required_action|roll_phase|harvest_phase|play_turn",
"citizen_id": 123, // for hire_citizen
"domain_id": 456, // for build_domain
"monster_id": 789, // for slay_monster
"gold_cost": 5, // for hire_citizen/build_domain
"strength_cost": 3, // for slay_monster
"magic_cost": 0, // optional
"action": "choose 1" // for act_on_required_action
}
```
## Web Client
Visit `http://localhost:8000` for a simple HTML client to test the API.
## Development Notes
- Games are stored in-memory (will be lost on server restart)
- Inactive games are cleaned up after 3 minutes of no activity
- Inactive lobby players are removed after 60 seconds
- This is a development/testing server, not production-ready

64
docs/database.md Normal file
View File

@@ -0,0 +1,64 @@
# Database
## Overview
The game bootstrap in `game.py` loads card data from a MariaDB database named `vckonline` using stored procedures to select card sets and randomize stacks.
The code assumes it can connect to:
- host: `127.0.0.1`
- port: `3306`
- user: `vckonline`
- password: `vckonline`
- database: `vckonline`
This is designed to work with an SSH tunnel that forwards the remote DB to local port 3306.
## SSH tunnel
Keep an SSH port forward running while using the DB locally:
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
## Stored procedures
The server/game code expects these procedures to exist:
- `select_base1_monsters()`
- `select_base1_citizens()`
- `select_base2_monsters()`
- `select_base2_citizens()`
- `select_random_domains()`
- `select_random_dukes()`
To install all procedures:
```bash
./sql/run_sql.sh sql/create_all_stored_procedures.sql
```
See `sql/INSTALL_PROCEDURES.md` for additional options (mysql client, interactive MariaDB session, installing individually).
## User / grants setup
If you have authentication or permissions problems, use:
- `sql/USER_SETUP_GUIDE.md`: investigation and fix commands (create users for `localhost`, `127.0.0.1`, `%`, and grant privileges)
- `sql/fix_user_setup.sql`: a convenience SQL script in this repo (if you prefer to run a script vs copy/paste commands)
## Verifying the DB
Quick port-level check (no Python deps):
```bash
python3 check_db_server.py
```
Full end-to-end check (Python + DB + tables + stored procs):
```bash
python3 test_database.py
```

33
docs/dev-setup.md Normal file
View File

@@ -0,0 +1,33 @@
# Dev setup
## Python environment
This repo expects a Python venv in `.venv/`.
The easiest path on macOS is to use the helper script:
```bash
./setup_venv.sh
```
That script:
- Creates (or reuses) `.venv/`
- Tries to locate `mariadb_config` under `/opt/homebrew`
- Installs `mariadb-connector-c` via Homebrew if needed
- Exports `MARIADB_CONFIG` and runs `pip install -r requirements.txt`
If you already have `.venv/` created and just want to activate with the MariaDB environment variable:
```bash
source ./activate_with_env.sh
```
## Requirements
Dependencies are listed in `requirements.txt` and include:
- `fastapi` + `uvicorn` (API server)
- `mariadb` (DB access; requires MariaDB Connector/C to build)
- `shortuuid` (player id generation)

121
docs/game.md Normal file
View File

@@ -0,0 +1,121 @@
# Game engine (`game.py`)
## Key types
`game.py` defines the core runtime objects used by the server:
- `Game`: contains the current game board state and implements actions/phases
- `Player`: per-player scores and owned cards
- `LobbyMember` / `GameMember`: lightweight records used by the server for lobby/game membership
It also provides:
- `load_game_data(...)`: builds an initial `game_state` dict by pulling data from MariaDB and dealing stacks
- `SummaryEncoder` and `GameObjectEncoder`: JSON encoders used by the server to serialize game state
## Game lifecycle
At runtime (via the server):
- `server.py` calls `load_game_data(game_id, preset, game_gamers)` to create a starting `game_state`
- The server wraps that dict in a `Game` object: `Game(game_state)`
- The server exposes the game via `/api/game/{game_id}/state` and `/api/game/{game_id}/action`
The `Game` object tracks:
- `player_list`
- `monster_grid`, `citizen_grid`, `domain_grid`
- dice: `die_one`, `die_two`, `die_sum`
- `effects`
- `action_required` (used to block on per-player, sequential “choose …” actions)
- `concurrent_action` (used to block on non-ordered, multi-player prompts; see “Concurrent actions” below)
- `last_active_time` (used by the server for cleanup)
## DB-backed bootstrap (`load_game_data`)
`load_game_data` is responsible for:
- Fetching cards using stored procedures:
- citizens/monsters depend on the `preset` (e.g. `"base1"` / `"base2"`)
- domains and dukes are randomized via procedures
- starters are selected via a direct `SELECT * FROM starters`
- Creating `Player` instances from the lobby/game membership list
- Randomizing player order and dealing initial cards
- Dealing stacks onto the board:
- monsters grouped by area, then 5 areas selected
- citizens grouped by roll match, placed into 10 stacks (special-cased for roll 11)
- domains dealt into 5 stacks of 3, with the top visible/accessible
This function currently assumes local DB connectivity via `127.0.0.1:3306` (typically via SSH port forward).
See `docs/database.md` for DB setup and stored procedure installation.
## Actions & phases
The server routes map `action_type` strings to methods on `Game`.
Common paths:
- `roll_phase()` rolls two dice and computes a sum
- `harvest_phase()` pays out from owned starters/citizens for all players based on the roll
- `hire_citizen(...)`, `build_domain(...)`, `slay_monster(...)` mutate board stacks and player resources
### “choose …” actions
Some special payouts set `action_required` and start a background thread that waits until `act_on_required_action` updates `action_required` with a choice.
This is a dev-oriented approach; it allows the REST API to supply a follow-up choice via `act_on_required_action` while the game engine waits.
## Concurrent actions (non-ordered prompts)
Some prompts are not turn-based: every participating player should be able to
respond at the same time, in any order, and the game must wait until **all**
of them have submitted before progressing. The starting duke selection is the
first example — every player simultaneously discards down to one of the dukes
they were dealt.
These are modeled with `Game.concurrent_action`, which is independent from
`action_required`:
```
concurrent_action = {
"kind": "choose_duke", # routes to a handler in CONCURRENT_HANDLERS
"pending": ["pid1", "pid3"],
"completed": ["pid2"],
"responses": { "pid2": <opaque payload> },
"data": { ... } # handler-specific extras (often empty)
}
```
Engine semantics:
- While `concurrent_action.pending` is non-empty, `advance_tick()` returns
`False`. No phase transitions happen, no harvest progresses, and no
per-player turn actions are accepted. `is_blocked_on_concurrent_action()`
exposes the same predicate.
- Players submit via `Game.submit_concurrent_action(player_id, response, kind=...)`.
The handler's `apply()` validates and applies that player's response
immediately (so per-player effects don't have to wait for the others).
- When the last pending player submits, the handler's `finalize()` runs
(for any cross-player resolution), `concurrent_action` is cleared, and
if the engine was sitting in setup it advances forward.
### Adding a new concurrent action kind
1. Implement a handler class with:
- `apply(self, game, player_id, response)` — validate + apply per-player
side effects. Raise `ValueError` to reject a submission (the player
stays in `pending`).
- `finalize(self, game)` — optional; runs once after every participant
has submitted.
2. Register it in `CONCURRENT_HANDLERS` keyed by `kind`.
3. Build the prompt with `_new_concurrent_action(kind, participant_ids, data=...)`
and assign it to `game.concurrent_action` at the point in the engine
where the gate should appear.
4. On the client, register a renderer in the `CONCURRENT_RENDERERS` map in
the dev client (server.py HTML) keyed on the same `kind`.
Because the engine itself only knows "block while pending is non-empty",
the concurrent gate is fully reusable — no engine changes are required to
add new kinds (mulligan, simultaneous discard, voting, etc.).

88
docs/server.md Normal file
View File

@@ -0,0 +1,88 @@
# Server (`server.py`)
## What it is
`server.py` is a FastAPI development server that:
- Maintains an in-memory lobby (`lobby`)
- Starts games when all lobby players are ready
- Stores active games in-memory (`games`)
- Exposes REST endpoints for lobby operations and game actions
- Serves a simple HTML test client at `/`
This server is intended for development/testing, not production.
## In-memory state
The server keeps three top-level collections:
- `lobby`: list of `LobbyMember` (players waiting to start a game)
- `games`: dict of `game_id -> Game`
- `gamers`: list of `GameMember` (player_id/name/game_id records for in-game players)
There is no persistence; restarting the server resets everything.
## Lobby flow
High-level flow:
- `POST /api/lobby/join`: creates a `LobbyMember` with a `shortuuid` `player_id`
- `POST /api/lobby/ready`: marks the player ready; when all lobby players are ready (and there are at least 2), it starts a game:
- generates a new `game_id` (uuid4)
- moves ready lobby members into `gamers` for that `game_id`
- calls `load_game_data(game_id, "base1", game_gamers)` and constructs `Game(game_state)`
- `GET /api/lobby/status`: returns lobby members + whether the requesting player is already in a game
Lobby cleanup:
- `GET /api/lobby/status` prunes lobby members inactive for > 60 seconds
## Game API
- `GET /api/game/{game_id}/state`: returns the current game state encoded using `GameObjectEncoder` (from `game.py`)
- `POST /api/game/{game_id}/action`: performs a game action and returns the updated game state
Supported `action_type` values currently include:
- `hire_citizen`
- `build_domain`
- `slay_monster`
- `take_resource`
- `harvest_card`
- `act_on_required_action` (sequential, single-player follow-ups)
- `submit_concurrent_action` (non-ordered, multi-player prompts; see below)
- `roll_phase`
- `harvest_phase`
- `play_turn`
### `submit_concurrent_action`
Used to respond to a `concurrent_action` gate (see `docs/game.md`). The
serialized game state exposes a `concurrent_action` object with `kind`,
`pending`, and `completed` lists; while `pending` is non-empty no other
turn-based action will succeed. Request body:
```
{
"player_id": "<pid>",
"action_type": "submit_concurrent_action",
"kind": "choose_duke", // optional sanity check
"response": "<opaque string>" // handler-specific payload
}
```
The server validates that the player is in `pending` and that `kind`
(if provided) matches the active gate. Players may submit in any order;
when the last pending player submits, the engine auto-advances out of
the setup gate.
## Game cleanup
On startup, a background task deletes games inactive for > 180 seconds and prunes the corresponding `gamers` entries.
## Dev HTML client
The root route `/` serves a simple HTML page that calls the lobby endpoints and can fetch a game state.
For run instructions, see `docs/README_SERVER.md`.

31
docs/testing.md Normal file
View File

@@ -0,0 +1,31 @@
# Testing & diagnostics
## Database connectivity
### Port-level check
`check_db_server.py` checks whether `127.0.0.1:3306` is reachable (useful to verify your SSH tunnel).
```bash
python3 check_db_server.py
```
### End-to-end DB validation
`test_database.py` does a more complete validation:
- imports `mariadb`
- connects to the DB
- checks required tables exist
- prints row counts and card contents (citizens/monsters/domains/dukes/starters)
- checks required stored procedures exist
- calls stored procedures and prints returned rows
```bash
python3 test_database.py
```
## API server smoke test
See `docs/README_SERVER.md` to run the FastAPI server and use the built-in HTML client at `/`.

View File

View File

2744
game.py Normal file

File diff suppressed because it is too large Load Diff

90
game_models.py Normal file
View File

@@ -0,0 +1,90 @@
from cards import Citizen, Domain, Duke, Monster, Starter
class Player:
def __init__(self, player_id, name):
self.player_id = player_id
self.name = name
self.owned_starters = []
self.owned_citizens = []
self.owned_domains = []
self.owned_dukes = []
self.owned_monsters = []
self.gold_score = 2
self.strength_score = 0
self.magic_score = 1
self.victory_score = 0
self.is_first = False
self.shadow_count = 0
self.holy_count = 0
self.soldier_count = 0
self.worker_count = 0
self.effects = {
"roll_phase": [],
"harvest_phase": [],
"action_phase": [],
}
self.harvest_delta = {"gold": 0, "strength": 0, "magic": 0, "victory": 0}
@classmethod
def from_dict(cls, data):
player_id = data["player_id"]
name = data["name"]
player = cls(player_id, name)
player.owned_starters = [Starter.from_dict(s) for s in data["owned_starters"]]
player.owned_citizens = [Citizen.from_dict(c) for c in data["owned_citizens"]]
player.owned_domains = [Domain.from_dict(d) for d in data["owned_domains"]]
player.owned_dukes = [Duke.from_dict(d) for d in data["owned_dukes"]]
player.owned_monsters = [Monster.from_dict(m) for m in data["owned_monsters"]]
player.gold_score = data["gold_score"]
player.strength_score = data["strength_score"]
player.magic_score = data["magic_score"]
player.victory_score = data["victory_score"]
player.is_first = data["is_first"]
player.effects = data["effects"]
player.harvest_delta = data.get("harvest_delta", {"gold": 0, "strength": 0, "magic": 0, "victory": 0})
roles = player.calc_roles()
player.shadow_count = roles["shadow_count"]
player.holy_count = roles["holy_count"]
player.soldier_count = roles["soldier_count"]
player.worker_count = roles["worker_count"]
return player
def calc_roles(self):
shadow_count = 0
holy_count = 0
soldier_count = 0
worker_count = 0
for citizen in self.owned_citizens:
shadow_count = shadow_count + citizen.shadow_count
holy_count = holy_count + citizen.holy_count
soldier_count = soldier_count + citizen.soldier_count
worker_count = worker_count + citizen.worker_count
for domain in self.owned_domains:
shadow_count = shadow_count + domain.shadow_count
holy_count = holy_count + domain.holy_count
soldier_count = soldier_count + domain.soldier_count
worker_count = worker_count + domain.worker_count
roles_dict = {
"shadow_count": shadow_count,
"holy_count": holy_count,
"soldier_count": soldier_count,
"worker_count": worker_count,
}
return roles_dict
class LobbyMember:
def __init__(self, player_name, player_id):
self.name = player_name
self.player_id = player_id
self.is_ready = False
self.debug_starting_resources = False
self.last_active_time = 0
class GameMember:
def __init__(self, player_id, player_name, game_id):
self.name = player_name
self.player_id = player_id
self.game_id = game_id

111
game_serialization.py Normal file
View File

@@ -0,0 +1,111 @@
from json import JSONEncoder
from cards import Citizen, Domain, Duke, Monster, Starter
from game_models import GameMember, LobbyMember, Player
class SummaryEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Player):
return {
"player_id": obj.player_id,
"name": obj.name,
"owned_citizens": len(obj.owned_citizens),
"owned_domains": len(obj.owned_domains),
"owned_monsters": len(obj.owned_monsters),
"gold_score": obj.gold_score,
"strength_score": obj.strength_score,
"magic_score": obj.magic_score,
"victory_score": obj.victory_score,
"is_first": obj.is_first,
}
if isinstance(obj, LobbyMember):
return {
"player_name": obj.name,
"player_id": obj.player_id,
"is_ready": obj.is_ready,
}
if isinstance(obj, GameMember):
return {
"player_name": obj.name,
"player_id": obj.player_id,
}
if hasattr(obj, "game_id") and hasattr(obj, "player_list"):
return {
"game_id": obj.game_id,
"player_list": obj.player_list,
}
return super().default(obj)
class GameObjectEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Player):
roles = obj.calc_roles()
return {
"player_id": obj.player_id,
"name": obj.name,
"owned_starters": [starter.to_dict() for starter in obj.owned_starters],
"owned_citizens": [citizen.to_dict() for citizen in obj.owned_citizens],
"owned_domains": [domain.to_dict() for domain in obj.owned_domains],
"owned_dukes": [duke.to_dict() for duke in obj.owned_dukes],
"owned_monsters": [monster.to_dict() for monster in obj.owned_monsters],
"gold_score": obj.gold_score,
"strength_score": obj.strength_score,
"magic_score": obj.magic_score,
"victory_score": obj.victory_score,
"is_first": obj.is_first,
"shadow_count": roles["shadow_count"],
"holy_count": roles["holy_count"],
"soldier_count": roles["soldier_count"],
"worker_count": roles["worker_count"],
"effects": obj.effects,
"harvest_delta": getattr(obj, "harvest_delta", {"gold": 0, "strength": 0, "magic": 0, "victory": 0}),
}
if isinstance(obj, Duke):
return obj.to_dict()
if isinstance(obj, Monster):
return obj.to_dict()
if isinstance(obj, Starter):
return obj.to_dict()
if isinstance(obj, Citizen):
return obj.to_dict()
if isinstance(obj, Domain):
return obj.to_dict()
if hasattr(obj, "game_id") and hasattr(obj, "player_list") and hasattr(obj, "monster_grid"):
ca_raw = getattr(obj, "concurrent_action", None)
ca_enc = ca_raw
if isinstance(ca_raw, dict) and not (ca_raw.get("pending") or []):
ca_enc = None
return {
"game_id": obj.game_id,
"player_list": obj.player_list,
"monster_grid": obj.monster_grid,
"citizen_grid": obj.citizen_grid,
"domain_grid": obj.domain_grid,
"die_one": obj.die_one,
"die_two": obj.die_two,
"die_sum": obj.die_sum,
"rolled_die_one": getattr(obj, "rolled_die_one", obj.die_one),
"rolled_die_two": getattr(obj, "rolled_die_two", obj.die_two),
"rolled_die_sum": getattr(obj, "rolled_die_sum", obj.die_sum),
"pending_roll": getattr(obj, "pending_roll", None),
"exhausted_count": obj.exhausted_count,
"effects": obj.effects,
"action_required": obj.action_required,
"pending_required_choice": getattr(obj, "pending_required_choice", None),
"pending_action_end_queue": getattr(obj, "pending_action_end_queue", None) or [],
"concurrent_action": ca_enc,
"tick_id": getattr(obj, "tick_id", 0),
"turn_number": getattr(obj, "turn_number", 1),
"turn_index": getattr(obj, "turn_index", 0),
"phase": getattr(obj, "phase", "roll"),
"actions_remaining": getattr(obj, "actions_remaining", 0),
"active_player_id": obj.current_player_id() if hasattr(obj, "current_player_id") else None,
"harvest_player_order": getattr(obj, "harvest_player_order", None),
"harvest_player_idx": getattr(obj, "harvest_player_idx", 0),
"harvest_consumed": getattr(obj, "harvest_consumed", {}) or {},
"harvest_prompt_slots": obj.harvest_slots_for_api() if hasattr(obj, "harvest_slots_for_api") else [],
"game_log": list(getattr(obj, "game_log", None) or []),
}
return super().default(obj)

283
game_setup.py Normal file
View File

@@ -0,0 +1,283 @@
import random
from typing import List
from cards import Citizen, Domain, Duke, Monster, Starter
from game_models import Player
def load_game_data(game_id, preset, player_list_from_lobby, debug_starting_resources=False):
import mariadb
monster_query = ""
monster_stack = []
citizen_query = ""
citizen_stack = []
domain_query = "select_random_domains"
domain_stack = []
duke_query = "select_random_dukes"
duke_stack = []
starter_query = "SELECT * FROM starters"
starter_stack = []
player_list = []
citizen_grid: List[List[Citizen]] = [[] for _ in range(10)]
domain_grid: List[List[Domain]] = [[] for _ in range(5)]
monster_grid: List[List[Monster]] = [[] for _ in range(5)]
die_one = 0
die_two = 0
die_sum = 0
exhausted_count = 0
effects = {
"roll_phase": [],
"harvest_phase": [],
"action_phase": [],
}
action_required = {
"id": "",
"action": "",
}
tick_id = 0
turn_number = 1
turn_index = 0
# Start in setup; if no setup actions are needed the engine will advance into roll.
phase = "setup"
actions_remaining = 0
harvest_processed = False
pending_harvest_choices = []
match preset:
case "base1":
monster_query = "select_base1_monsters"
citizen_query = "select_base1_citizens"
case "base2":
monster_query = "select_base2_monsters"
citizen_query = "select_base2_citizens"
try:
my_connect = mariadb.connect(
user="vckonline", password="vckonline", host="127.0.0.1", database="vckonline", port=3306
)
my_cursor = my_connect.cursor(dictionary=True)
my_cursor.callproc(monster_query)
results = my_cursor.fetchall()
for row in results:
my_monster = Monster(
row["id_monsters"],
row["name"],
row["area"],
row["monster_type"],
row["monster_order"],
row["strength_cost"],
row["magic_cost"],
row["vp_reward"],
row["gold_reward"],
row["strength_reward"],
row["magic_reward"],
row["has_special_reward"],
row["special_reward"],
row["has_special_cost"],
row["special_cost"],
row["is_extra"],
row["expansion"],
)
monster_stack.append(my_monster)
my_cursor.callproc(citizen_query)
citizen_count = 5
if len(player_list_from_lobby) == 5:
citizen_count = 6
results = my_cursor.fetchall()
for row in results:
for _ in range(citizen_count):
my_citizen = Citizen(
row["id_citizens"],
row["name"],
row["gold_cost"],
row["roll_match1"],
row["roll_match2"],
row["shadow_count"],
row["holy_count"],
row["soldier_count"],
row["worker_count"],
row["gold_payout_on_turn"],
row["gold_payout_off_turn"],
row["strength_payout_on_turn"],
row["strength_payout_off_turn"],
row["magic_payout_on_turn"],
row["magic_payout_off_turn"],
row["has_special_payout_on_turn"],
row["has_special_payout_off_turn"],
row["special_payout_on_turn"],
row["special_payout_off_turn"],
row["special_citizen"],
row["expansion"],
)
citizen_stack.append(my_citizen)
my_cursor.callproc(domain_query)
results = my_cursor.fetchall()
for row in results:
my_domain = Domain(
row["id_domains"],
row["name"],
row["gold_cost"],
row["shadow_count"],
row["holy_count"],
row["soldier_count"],
row["worker_count"],
row["vp_reward"],
row["has_activation_effect"],
row["has_passive_effect"],
row["passive_effect"],
row["activation_effect"],
row["text"],
row["expansion"],
)
domain_stack.append(my_domain)
my_cursor.callproc(duke_query)
results = my_cursor.fetchall()
for row in results:
my_duke = Duke(
row["id_dukes"],
row["name"],
row["gold_mult"],
row["strength_mult"],
row["magic_mult"],
row["shadow_mult"],
row["holy_mult"],
row["soldier_mult"],
row["worker_mult"],
row["monster_mult"],
row["citizen_mult"],
row["domain_mult"],
row["boss_mult"],
row["minion_mult"],
row["beast_mult"],
row["titan_mult"],
row["expansion"],
)
duke_stack.append(my_duke)
my_cursor.execute(starter_query)
my_result = my_cursor.fetchall()
for row in my_result:
my_starter = Starter(
row["id_starters"],
row["name"],
row["roll_match1"],
row["roll_match2"],
row["gold_payout_on_turn"],
row["gold_payout_off_turn"],
row["strength_payout_on_turn"],
row["strength_payout_off_turn"],
row["magic_payout_on_turn"],
row["magic_payout_off_turn"],
row["has_special_payout_on_turn"],
row["has_special_payout_off_turn"],
row["special_payout_on_turn"],
row["special_payout_off_turn"],
row["expansion"],
)
starter_stack.append(my_starter)
my_cursor.close()
my_connect.close()
except Exception as e:
print(f"Error: {e}")
# create players and determine order
if not all([player_list_from_lobby, starter_query, monster_stack, citizen_stack, domain_stack, duke_stack]):
raise ValueError("One or more required lists are empty.")
else:
for player in player_list_from_lobby:
my_player = Player(player.player_id, player.name)
if debug_starting_resources:
my_player.gold_score = 100
my_player.strength_score = 100
my_player.magic_score = 100
player_list.append(my_player)
random.shuffle(player_list)
player_list[0].is_first = True
# give players starters and dukes
for player in player_list:
player.owned_starters.append(starter_stack[0])
player.owned_starters.append(starter_stack[1])
for _ in range(2):
player.owned_dukes.append(duke_stack.pop())
# deal monsters onto the board
grouped_monsters = {}
for monster in monster_stack:
area = monster.area
if area in grouped_monsters:
grouped_monsters[area].append(monster)
else:
grouped_monsters[area] = [monster]
for area, monsters in grouped_monsters.items():
monsters.sort(key=lambda item: item.order, reverse=True)
areas = list(grouped_monsters.keys())
chosen_areas = random.sample(areas, 5)
for i, area in enumerate(chosen_areas):
monsters = grouped_monsters[area]
monster_grid[i].extend(monsters)
for stack in monster_grid:
for monster in stack:
monster.toggle_visibility(True)
stack[-1].toggle_accessibility(True)
# deal citizens onto the board
citizens_by_roll = {roll: [] for roll in [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]}
for citizen in citizen_stack:
citizen.toggle_visibility()
citizens_by_roll[citizen.roll_match1].append(citizen)
for roll in citizens_by_roll:
index = roll - 1 if roll < 11 else 9
citizens = citizens_by_roll[roll]
citizen_grid[index].extend(list(citizens))
citizen_grid[index][-1].toggle_accessibility(True)
if debug_starting_resources:
for player in player_list:
for stack in citizen_grid:
c = stack.pop(-1)
c.is_flipped = False
c.toggle_visibility(True)
c.toggle_accessibility(True)
player.owned_citizens.append(c)
if stack:
stack[-1].toggle_accessibility(True)
# deal the domains into stacks
for i in range(5):
stack = domain_grid[i]
for j in range(3):
if j == 2:
domain = domain_stack.pop()
domain.toggle_visibility(True)
domain.toggle_accessibility(True)
stack.append(domain)
else:
domain = domain_stack.pop()
stack.append(domain)
game_state = {
"game_id": game_id,
"player_list": player_list,
"monster_grid": monster_grid,
"citizen_grid": citizen_grid,
"domain_grid": domain_grid,
"die_one": die_one,
"die_two": die_two,
"die_sum": die_sum,
"exhausted_count": exhausted_count,
"effects": effects,
"action_required": action_required,
"concurrent_action": None,
"tick_id": tick_id,
"turn_number": turn_number,
"turn_index": turn_index,
"phase": phase,
"actions_remaining": actions_remaining,
"harvest_processed": harvest_processed,
"pending_harvest_choices": pending_harvest_choices,
"harvest_player_order": None,
"harvest_player_idx": 0,
"harvest_consumed": {},
"game_log": [],
"pending_action_end_queue": [],
}
return game_state

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
images/citizen_07_rogue.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
images/monster_25_giant.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
images/monster_26_giant.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
images/monster_28_troll.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

Before

Width:  |  Height:  |  Size: 555 KiB

After

Width:  |  Height:  |  Size: 555 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 4.8 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 4.5 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 429 KiB

After

Width:  |  Height:  |  Size: 429 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 206 KiB

View File

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 252 KiB

22
requirements.txt Normal file
View File

@@ -0,0 +1,22 @@
# Core database connection
# NOTE: mariadb package requires MariaDB Connector/C to be installed first
#
# Installation:
# 1. Install MariaDB Connector/C:
# macOS: brew install mariadb-connector-c
# Linux: sudo apt-get install libmariadb-dev (Debian/Ubuntu) or equivalent
#
# 2. Set MARIADB_CONFIG environment variable before pip install:
# export MARIADB_CONFIG="/opt/homebrew/Cellar/mariadb-connector-c/3.4.8/bin/mariadb_config"
# (or run: ./setup_venv.sh which does this automatically)
#
# 3. Then install: pip install -r requirements.txt
mariadb>=1.1.0
# UUID generation
shortuuid>=1.0.0
# Web framework for API server
fastapi>=0.104.0
uvicorn>=0.24.0

791
server.py Executable file → Normal file
View File

@@ -1,365 +1,464 @@
import socket
#!/usr/bin/env python3
"""
FastAPI server for VCK Online - Development/testing server
Simple REST API to replace the socket-based protocol
"""
import time
import threading
from common import *
import uuid
from pathlib import Path
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import shortuuid
from game import Game, LobbyMember, GameMember, load_game_data, GameObjectEncoder
import json
import mariadb
_REPO_ROOT = Path(__file__).resolve().parent
_DEV_CLIENT_INDEX = _REPO_ROOT / "static" / "dev-client" / "index.html"
app = FastAPI(title="VCK Online API", description="Development server for Valeria Card Kingdoms Online")
# CORS middleware for web client
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory storage (simple for dev)
lobby: List[LobbyMember] = []
games: Dict[str, Game] = {}
gamers: List[GameMember] = []
class ServerVCKO:
def __init__(self):
self.host = socket.gethostbyname(socket.gethostname())
self.server_socket = socket.socket()
self.server_socket.bind((self.host, Constants.port))
self.game_dict = {}
self.lobby = []
self.gamers = []
# Request/Response models
class JoinLobbyRequest(BaseModel):
name: str
# Start the thread to remove inactive players
self.inactive_player_thread = threading.Thread(target=self.remove_inactive_players, daemon=True)
self.inactive_player_thread.start()
def remove_inactive_players(self):
while True:
current_time = time.time()
self.lobby = [player for player in self.lobby if current_time - player.last_active_time <= 60]
time.sleep(10) # check for inactive players every 10 seconds
class RenameRequest(BaseModel):
player_id: str
name: str
def handle_client(self, conn, addr):
print(f"Connection from: {addr}")
connected = True
while connected:
msg_length = conn.recv(Constants.header_size).decode(Constants.text_format)
if msg_length:
msg_length = int(msg_length)
msg = conn.recv(msg_length).decode(Constants.text_format)
first_word = msg.split()[0]
full_command = msg.split()
match first_word:
case "connection_check":
connected = False
send_data(conn, "received".encode(Constants.encoding))
case "server":
connected = False
response_dict = {
"threads": threading.active_count(),
"lobby": self.lobby,
"game_count": len(self.game_dict),
"games": self.game_dict
}
lobby_state = json.dumps(response_dict, cls=SummaryEncoder, indent=2)
send_data(conn, lobby_state.encode(Constants.encoding))
case "lobby":
connected = False
if full_command[1] == "join" and len(full_command) > 2:
joining_player_name = ' '.join(full_command[2:])
joining_player_id = str(shortuuid.uuid())
joining_player = LobbyMember(joining_player_name, joining_player_id)
self.lobby.append(joining_player)
message = f"lobby joined {joining_player_id}"
send_data(conn, message.encode(Constants.encoding))
elif full_command[1] == "rename" and len(full_command) > 3:
for player in self.lobby:
if player.player_id == full_command[2]:
player.name = ' '.join(full_command[3:])
message = f"lobby renamed {player.player_id}"
send_data(conn, message.encode(Constants.encoding))
else:
send_data(conn, "invalid message".encode(Constants.encoding))
elif full_command[1] == "leave" and len(full_command) > 2:
temp_lobby = []
for player in self.lobby:
if player.player_id != full_command[2]:
temp_lobby.append(player)
self.lobby = temp_lobby
self.send_lobby_state(conn)
elif full_command[1] == "get_status" and len(full_command) >= 2:
found = False
if len(full_command) == 3:
for player in self.lobby:
if full_command[2] == player.player_id:
player.last_active_time = time.time() # update last active time
self.send_lobby_state(conn)
for player in self.gamers:
if full_command[2] == player.player_id:
message = f"game joined {player.game_id}"
send_data(conn, message.encode(Constants.encoding))
else:
self.send_lobby_state(conn)
elif full_command[1] == "ready" and len(full_command) > 2:
ready_check = 0
for player in self.lobby:
if player.player_id == full_command[2]:
class ReadyRequest(BaseModel):
player_id: str
debug_starting_resources: bool = False
class ResourcePayment(BaseModel):
"""How much gold / strength / magic the client spends on an action (validated server-side)."""
gold: int = 0
strength: int = 0
magic: int = 0
class GameActionRequest(BaseModel):
player_id: str
action_type: str # "hire_citizen", "build_domain", "slay_monster", "take_resource", "act_on_required_action", "submit_concurrent_action"
# Action parameters (varies by action type)
citizen_id: Optional[int] = None
domain_id: Optional[int] = None
monster_id: Optional[int] = None
# take_resource: "gold" | "strength" | "magic"
resource: Optional[str] = None
gold_cost: Optional[int] = None
strength_cost: Optional[int] = None
magic_cost: Optional[int] = None
# Preferred: explicit payment split (gold/strength/magic). If set, overrides legacy *_cost fields for that action.
payment: Optional[ResourcePayment] = None
action: Optional[str] = None # For act_on_required_action
harvest_slot_key: Optional[str] = None # harvest_card: e.g. "citizen:3:0"
# submit_concurrent_action: which non-ordered prompt this responds to,
# plus the opaque per-player payload (string; handler decides how to parse).
kind: Optional[str] = None
response: Optional[str] = None
# finalize_roll: optional override dice values (1-6). If omitted, server finalizes using the rolled dice.
die_one: Optional[int] = None
die_two: Optional[int] = None
def _rollback_consumed_action(game):
game.actions_remaining = int(getattr(game, "actions_remaining", 0)) + 1
game.tick_id = int(getattr(game, "tick_id", 0)) - 1
def resolve_action_payment(req: GameActionRequest):
if req.payment is not None:
return int(req.payment.gold or 0), int(req.payment.strength or 0), int(req.payment.magic or 0)
if req.action_type == "slay_monster":
return 0, int(req.strength_cost or 0), int(req.magic_cost or 0)
if req.action_type == "hire_citizen":
return int(req.gold_cost or 0), 0, int(req.magic_cost or 0)
if req.action_type == "build_domain":
return int(req.gold_cost or 0), 0, int(req.magic_cost or 0)
return int(req.gold_cost or 0), int(req.strength_cost or 0), int(req.magic_cost or 0)
# Lobby endpoints
@app.post("/api/lobby/join")
async def join_lobby(request: JoinLobbyRequest):
"""Join the lobby with a player name"""
player_id = str(shortuuid.uuid())
player = LobbyMember(request.name, player_id)
player.last_active_time = time.time()
lobby.append(player)
return {"player_id": player_id, "message": "Joined lobby"}
@app.post("/api/lobby/rename")
async def rename_player(request: RenameRequest):
"""Rename a player in the lobby"""
for player in lobby:
if player.player_id == request.player_id:
player.name = request.name
player.last_active_time = time.time()
return {"message": "Player renamed"}
raise HTTPException(status_code=404, detail="Player not found in lobby")
@app.post("/api/lobby/leave")
async def leave_lobby(player_id: str):
"""Leave the lobby"""
global lobby
lobby = [p for p in lobby if p.player_id != player_id]
return {"message": "Left lobby"}
@app.post("/api/lobby/ready")
async def set_ready(request: ReadyRequest):
"""Mark player as ready"""
for player in lobby:
if player.player_id == request.player_id:
player.is_ready = True
if player.is_ready:
ready_check += 1
print(f"ready check: {ready_check}")
if ready_check == len(self.lobby):
player.debug_starting_resources = bool(request.debug_starting_resources)
player.last_active_time = time.time()
# Check if all players are ready
ready_count = sum(1 for p in lobby if p.is_ready)
if ready_count == len(lobby) and len(lobby) >= 2:
# Start game
new_game_id = str(uuid.uuid4())
for player in self.lobby:
print(f"lobby player: {player.name}")
players_to_remove = []
for player in self.lobby:
if player.is_ready:
new_gamer = GameMember(player.player_id, player.name, new_game_id)
self.gamers.append(new_gamer)
players_to_remove.append(player)
for player in players_to_remove:
self.lobby.remove(player)
# START GAME
new_game = Game(load_game_data(new_game_id, "base1", self.gamers))
self.game_dict[new_game.game_id] = new_game
print(f"size of game dict: {len(self.game_dict)}")
message = f"game joined {new_game_id}"
send_data(conn, message.encode(Constants.encoding))
else:
self.send_lobby_state(conn)
elif full_command[1] == "unready" and len(full_command) > 2:
for player in self.lobby:
if player.player_id == full_command[2]:
player.is_ready = False
self.send_lobby_state(conn)
else:
send_data(conn, "invalid message".encode(Constants.encoding))
case "game":
connected = False
if full_command[1] == "get_status" and len(full_command) == 3:
print(full_command[2])
print(len(self.game_dict))
for game in self.game_dict:
print(f"game id: {game}")
game_id = full_command[2]
game = self.game_dict.get(game_id)
if not game:
message = "game state error: game not found"
send_data(conn, message.encode(Constants.encoding))
else:
self.send_game_state(conn, full_command[2])
case _:
connected = False
send_data(conn, "invalid message".encode(Constants.encoding))
print(f"[{addr}] {msg}")
conn.close()
def start(self):
self.server_socket.listen()
print(f"server is listening on {socket.gethostbyname(socket.gethostname())}")
while True:
conn, addr = self.server_socket.accept()
thread = threading.Thread(target=self.handle_client, args=(conn, addr))
thread.start()
print(f"\nActive threads: {threading.active_count() - 1}")
for p in lobby:
if p.is_ready:
gamer = GameMember(p.player_id, p.name, new_game_id)
gamers.append(gamer)
players_to_remove.append(p)
debug_starting_resources = any(bool(getattr(p, "debug_starting_resources", False)) for p in players_to_remove)
def send_lobby_state(self, conn):
lobby_data = []
for lobby_member in self.lobby:
player_dict = {
"name": lobby_member.name,
"player_id": lobby_member.player_id,
"is_ready": lobby_member.is_ready
}
lobby_data.append(player_dict)
response = f"lobby state {json.dumps(lobby_data)}"
send_data(conn, response.encode(Constants.encoding))
# Remove ready players from lobby
for p in players_to_remove:
lobby.remove(p)
def send_game_state(self, conn, game_id):
game = self.game_dict.get(game_id)
if not game:
response = "game state error: game not found"
else:
game_json = json.dumps(game, cls=GameObjectEncoder, indent=2)
response = f"game state {game_json}"
send_data(conn, response.encode(Constants.encoding))
def load_game_data(game_id, preset, player_list_from_lobby):
monster_query = ""
monster_stack = []
citizen_query = ""
citizen_stack = []
domain_query = "select_random_domains"
domain_stack = []
duke_query = "select_random_dukes"
duke_stack = []
starter_query = "SELECT * FROM starters"
starter_stack = []
player_list = []
citizen_grid: List[List[Citizen]] = [[] for _ in range(10)]
domain_grid: List[List[Domain]] = [[] for _ in range(5)]
monster_grid: List[List[Monster]] = [[] for _ in range(5)]
die_one = 0
die_two = 0
die_sum = 0
exhausted_count = 0
effects = {
"roll_phase": [],
"harvest_phase": [],
"action_phase": []
}
action_required = {
"player_id": "",
"action": ""
}
match preset:
case "base1":
monster_query = "select_base1_monsters"
citizen_query = "select_base1_citizens"
case "base2":
monster_query = "select_base2_monsters"
citizen_query = "select_base2_citizens"
# Create game
try:
my_connect = mariadb.connect(user='vckonline', password='vckonline', host='127.0.0.1',
database='vckonline')
my_cursor = my_connect.cursor(dictionary=True)
my_cursor.callproc(monster_query)
results = my_cursor.fetchall()
for row in results:
my_monster = Monster(row['id_monsters'], row['name'], row['area'], row['monster_type'],
row['monster_order'], row['strength_cost'], row['magic_cost'], row['vp_reward'],
row['gold_reward'], row['strength_reward'], row['magic_reward'],
row['has_special_reward'], row['special_reward'], row['has_special_cost'],
row['special_cost'], row['is_extra'], row['expansion'])
monster_stack.append(my_monster)
my_cursor.callproc(citizen_query)
citizen_count = 5
if len(player_list_from_lobby) == 5:
citizen_count = 6
results = my_cursor.fetchall()
for row in results:
for i in range(citizen_count):
my_citizen = Citizen(row['id_citizens'], row['name'], row['gold_cost'], row['roll_match1'],
row['roll_match2'], row['shadow_count'], row['holy_count'], row['soldier_count'],
row['worker_count'], row['gold_payout_on_turn'], row['gold_payout_off_turn'],
row['strength_payout_on_turn'], row['strength_payout_off_turn'],
row['magic_payout_on_turn'], row['magic_payout_off_turn'],
row['has_special_payout_on_turn'], row['has_special_payout_off_turn'],
row['special_payout_on_turn'], row['special_payout_off_turn'],
row['special_citizen'],
row['expansion'])
citizen_stack.append(my_citizen)
my_cursor.callproc(domain_query)
results = my_cursor.fetchall()
for row in results:
my_domain = Domain(row['id_domains'], row['name'], row['gold_cost'], row['shadow_count'], row['holy_count'],
row['soldier_count'], row['worker_count'], row['vp_reward'],
row['has_activation_effect'], row['has_passive_effect'], row['passive_effect'],
row['activation_effect'], row['text'], row['expansion'])
domain_stack.append(my_domain)
my_cursor.callproc(duke_query)
results = my_cursor.fetchall()
for row in results:
my_duke = Duke(row['id_dukes'], row['name'], row['gold_mult'], row['strength_mult'], row['magic_mult'],
row['shadow_mult'], row['holy_mult'], row['soldier_mult'], row['worker_mult'],
row['monster_mult'], row['citizen_mult'], row['domain_mult'], row['boss_mult'],
row['minion_mult'], row['beast_mult'], row['titan_mult'], row['expansion'])
duke_stack.append(my_duke)
my_cursor.execute(starter_query)
my_result = my_cursor.fetchall()
for row in my_result:
my_starter = Starter(row['id_starters'], row['name'], row['roll_match1'], row['roll_match2'],
row['gold_payout_on_turn'], row['gold_payout_off_turn'],
row['strength_payout_on_turn'], row['strength_payout_off_turn'],
row['magic_payout_on_turn'], row['magic_payout_off_turn'],
row['has_special_payout_on_turn'], row['has_special_payout_off_turn'],
row['special_payout_on_turn'], row['special_payout_off_turn'], row['expansion'])
starter_stack.append(my_starter)
my_cursor.close()
my_connect.close()
# Get only the gamers for this new game
game_gamers = [g for g in gamers if g.game_id == new_game_id]
game_state = load_game_data(
new_game_id,
"base1",
game_gamers,
debug_starting_resources=debug_starting_resources,
)
new_game = Game(game_state)
new_game.last_active_time = time.time()
# Auto-run the start-of-game roll/harvest so the first state is actionable.
while new_game.advance_tick():
if getattr(new_game, "phase", None) == "action":
break
games[new_game_id] = new_game
return {
"message": "Game started",
"game_id": new_game_id,
"players": [{"player_id": g.player_id, "name": g.name} for g in game_gamers]
}
except Exception as e:
print(f"Error: {e}")
# print(f"size of monster stack: {len(monster_stack)}")
# print(f"size of citizen stack: {len(citizen_stack)}")
# print(f"size of domain stack: {len(domain_stack)}")
# print(f"size of duke stack: {len(duke_stack)}")
# print(f"size of starter stack: {len(starter_stack)}")
# create players and determine order
if not all([player_list_from_lobby, starter_query, monster_stack, citizen_stack, domain_stack, duke_stack]):
raise ValueError("One or more required lists are empty.")
raise HTTPException(status_code=500, detail=f"Failed to create game: {str(e)}")
return {"message": "Player ready", "all_ready": ready_count == len(lobby)}
raise HTTPException(status_code=404, detail="Player not found in lobby")
@app.post("/api/lobby/unready")
async def set_unready(request: ReadyRequest):
"""Mark player as not ready"""
for player in lobby:
if player.player_id == request.player_id:
player.is_ready = False
player.last_active_time = time.time()
return {"message": "Player unready"}
raise HTTPException(status_code=404, detail="Player not found in lobby")
@app.get("/api/lobby/status")
async def get_lobby_status(player_id: Optional[str] = None):
"""Get lobby status. If player_id provided, also check if player is in a game."""
# Clean up inactive players (60 seconds)
current_time = time.time()
global lobby
lobby = [p for p in lobby if current_time - p.last_active_time <= 60]
lobby_data = []
for member in lobby:
lobby_data.append({
"player_id": member.player_id,
"name": member.name,
"is_ready": member.is_ready,
"debug_starting_resources": bool(getattr(member, "debug_starting_resources", False)),
})
response = {
"lobby": lobby_data,
"game_count": len(games)
}
# Check if player is in a game
if player_id:
for gamer in gamers:
if gamer.player_id == player_id:
response["in_game"] = True
response["game_id"] = gamer.game_id
return response
response["in_game"] = False
return response
# Game endpoints
def _serialize_game_for_player(game, viewer_player_id: Optional[str]):
"""
Serialize the game state for a given viewer.
Security/hidden-info rule:
- Dukes are hidden information. Only the viewing player should see their own duke.
"""
game_json = json.dumps(game, cls=GameObjectEncoder, indent=2)
state = json.loads(game_json)
players = state.get("player_list") or []
if not isinstance(players, list):
return state
for p in players:
if not isinstance(p, dict):
continue
pid = p.get("player_id")
if not viewer_player_id or pid != viewer_player_id:
# Hide opponent (and spectator) dukes.
p["owned_dukes"] = []
return state
@app.get("/api/game/{game_id}/state")
async def get_game_state(game_id: str, player_id: Optional[str] = None):
"""Get the current game state"""
game = games.get(game_id)
if not game:
raise HTTPException(status_code=404, detail="Game not found")
game.last_active_time = time.time()
# Ensure the beginning-of-turn roll/harvest are automatic (including the very first fetch).
while getattr(game, "phase", None) in ("roll", "harvest"):
if not game.advance_tick():
break
if getattr(game, "phase", None) == "action":
break
return _serialize_game_for_player(game, player_id)
@app.post("/api/game/{game_id}/action")
async def perform_game_action(game_id: str, request: GameActionRequest):
"""Perform a game action (hire citizen, build domain, slay monster, etc.)"""
game = games.get(game_id)
if not game:
raise HTTPException(status_code=404, detail="Game not found")
game.last_active_time = time.time()
try:
if request.action_type == "hire_citizen":
if request.citizen_id is None:
raise HTTPException(status_code=400, detail="citizen_id required")
if request.payment is None and request.gold_cost is None and request.magic_cost is None:
raise HTTPException(status_code=400, detail="payment or gold_cost/magic_cost required")
if not game.consume_player_action(request.player_id):
raise HTTPException(status_code=400, detail="Not your turn (or no actions remaining)")
g, s, m = resolve_action_payment(request)
try:
game.hire_citizen(request.player_id, request.citizen_id, g, m, s)
except ValueError as e:
_rollback_consumed_action(game)
raise HTTPException(status_code=400, detail=str(e))
except Exception:
_rollback_consumed_action(game)
raise
game.finish_turn_if_no_actions_remaining()
elif request.action_type == "build_domain":
if request.domain_id is None:
raise HTTPException(status_code=400, detail="domain_id required")
if request.payment is None and request.gold_cost is None and request.magic_cost is None:
raise HTTPException(status_code=400, detail="payment or gold_cost/magic_cost required")
if not game.consume_player_action(request.player_id):
raise HTTPException(status_code=400, detail="Not your turn (or no actions remaining)")
g, s, m = resolve_action_payment(request)
try:
game.build_domain(request.player_id, request.domain_id, g, m, s)
except ValueError as e:
_rollback_consumed_action(game)
raise HTTPException(status_code=400, detail=str(e))
except Exception:
_rollback_consumed_action(game)
raise
game.finish_turn_if_no_actions_remaining()
elif request.action_type == "slay_monster":
if request.monster_id is None:
raise HTTPException(status_code=400, detail="monster_id required")
if request.payment is None and request.strength_cost is None and request.magic_cost is None:
raise HTTPException(status_code=400, detail="payment or strength_cost/magic_cost required")
if not game.consume_player_action(request.player_id):
raise HTTPException(status_code=400, detail="Not your turn (or no actions remaining)")
g, s, m = resolve_action_payment(request)
try:
game.slay_monster(request.player_id, request.monster_id, s, m, g)
except ValueError as e:
_rollback_consumed_action(game)
raise HTTPException(status_code=400, detail=str(e))
except Exception:
_rollback_consumed_action(game)
raise
game.finish_turn_if_no_actions_remaining()
elif request.action_type == "take_resource":
if request.resource is None or not str(request.resource).strip():
raise HTTPException(status_code=400, detail='resource required ("gold", "strength", or "magic")')
r = str(request.resource).strip().lower()
if r not in ("gold", "strength", "magic"):
raise HTTPException(status_code=400, detail='resource must be "gold", "strength", or "magic"')
if not game.consume_player_action(request.player_id):
raise HTTPException(status_code=400, detail="Not your turn (or no actions remaining)")
try:
game.take_resource(request.player_id, r)
except ValueError as e:
_rollback_consumed_action(game)
raise HTTPException(status_code=400, detail=str(e))
except Exception:
_rollback_consumed_action(game)
raise
game.finish_turn_if_no_actions_remaining()
elif request.action_type == "act_on_required_action":
if request.action is None:
raise HTTPException(status_code=400, detail="action required")
game.act_on_required_action(request.player_id, request.action)
# resolving a required action may unblock the engine
game.advance_tick()
elif request.action_type == "submit_concurrent_action":
if request.response is None:
raise HTTPException(status_code=400, detail="response required")
try:
game.submit_concurrent_action(
request.player_id,
request.response,
kind=request.kind,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
elif request.action_type == "finalize_roll":
try:
game.finalize_roll(request.player_id, die_one=request.die_one, die_two=request.die_two)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# After finalizing, auto-advance harvest as far as possible.
while getattr(game, "phase", None) == "harvest":
if not game.advance_tick():
break
if getattr(game, "phase", None) == "action":
break
elif request.action_type == "roll_phase":
raise HTTPException(status_code=400, detail="roll_phase is automatic; reserved for future reroll effects")
elif request.action_type == "harvest_phase":
raise HTTPException(status_code=400, detail="harvest_phase is automatic")
elif request.action_type == "harvest_card":
if not request.harvest_slot_key or not str(request.harvest_slot_key).strip():
raise HTTPException(status_code=400, detail="harvest_slot_key required")
try:
game.harvest_card(request.player_id, str(request.harvest_slot_key).strip())
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
elif request.action_type == "play_turn":
# Advance through roll+harvest, then leave at action phase
# (player actions will drive action ticks)
while game.advance_tick():
if game.phase == "action":
break
else:
for player in player_list_from_lobby:
my_player = Player(player.player_id, player.name)
player_list.append(my_player)
random.shuffle(player_list)
player_list[0].is_first = True
# give players starters and dukes
for player in player_list:
player.owned_starters.append(starter_stack[0])
player.owned_starters.append(starter_stack[1])
for i in range(2):
player.owned_dukes.append(duke_stack.pop())
# deal monsters onto the board
grouped_monsters = {}
for monster in monster_stack:
area = monster.area
if area in grouped_monsters:
grouped_monsters[area].append(monster)
else:
grouped_monsters[area] = [monster]
# Reverse the order of each group by monster_order
for area, monsters in grouped_monsters.items():
monsters.sort(key=lambda item: item.order, reverse=True)
areas = list(grouped_monsters.keys())
chosen_areas = random.sample(areas, 5)
for i, area in enumerate(chosen_areas):
monsters = grouped_monsters[area]
monster_grid[i].extend(monsters)
for i, stack in enumerate(monster_grid):
for monster in stack:
monster.toggle_visibility(True)
# Make the last monster in the stack accessible
stack[-1].toggle_accessibility(True)
monster_stack = []
# deal citizens onto the board
# Create a dictionary to store citizen lists with roll numbers as keys
citizens_by_roll = {roll: [] for roll in [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]}
# Group citizens by roll number
for citizen in citizen_stack:
citizen.toggle_visibility()
citizens_by_roll[citizen.roll_match1].append(citizen)
for roll in citizens_by_roll:
# Map 11 roll to index 9
index = roll - 1 if roll < 11 else 9
citizens = citizens_by_roll[roll]
citizen_grid[index].extend(list(citizens))
# Make the first citizen in each list accessible
citizen_grid[index][-1].toggle_accessibility(True)
citizen_stack = []
# Deal the domains into the stacks
for i in range(5):
stack = domain_grid[i]
for j in range(3):
if j == 2: # top domain is visible and accessible
domain = domain_stack.pop()
domain.toggle_visibility(True)
domain.toggle_accessibility(True)
stack.append(domain)
else: # other domains are not visible or accessible
domain = domain_stack.pop()
stack.append(domain)
raise HTTPException(status_code=400, detail=f"Unknown action type: {request.action_type}")
# Create a dictionary to store all the stacks
game_state = {'game_id': game_id,
'player_list': player_list,
'monster_grid': monster_grid,
'citizen_grid': citizen_grid,
'domain_grid': domain_grid,
'die_one': die_one,
'die_two': die_two,
'die_sum': die_sum,
'exhausted_count': exhausted_count,
'effects': effects,
'action_required': action_required}
# Return the dictionary
return game_state
# Return updated game state
return {"message": "Action performed", "game_state": _serialize_game_for_player(game, request.player_id)}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Action failed: {str(e)}")
if __name__ == '__main__':
print("server starting")
server = ServerVCKO()
server.start()
# Cleanup inactive games (runs periodically)
@app.on_event("startup")
async def startup_event():
"""Cleanup task for inactive games"""
import asyncio
async def cleanup():
while True:
await asyncio.sleep(30) # Check every 30 seconds
current_time = time.time()
inactive_games = [
game_id for game_id, game in games.items()
if current_time - game.last_active_time > 180
]
for game_id in inactive_games:
del games[game_id]
# Remove gamers from this game
global gamers
gamers = [g for g in gamers if g.game_id != game_id]
asyncio.create_task(cleanup())
# Serve static files and simple HTML client
try:
app.mount("/static", StaticFiles(directory="static"), name="static")
except:
pass # static directory might not exist
@app.get("/")
async def root():
"""Simple HTML client for testing"""
return FileResponse(_DEV_CLIENT_INDEX, media_type="text/html")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

40
setup_venv.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Setup script for VCK Online virtual environment
# Find mariadb_config
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
if [ -z "$MARIADB_CONFIG_PATH" ]; then
echo "Error: mariadb_config not found. Installing mariadb-connector-c..."
brew install mariadb-connector-c
MARIADB_CONFIG_PATH=$(find /opt/homebrew -name mariadb_config 2>/dev/null | head -1)
fi
if [ -z "$MARIADB_CONFIG_PATH" ]; then
echo "Error: Could not find mariadb_config after installation."
echo "Please install manually: brew install mariadb-connector-c"
exit 1
fi
echo "Found mariadb_config at: $MARIADB_CONFIG_PATH"
echo "Setting MARIADB_CONFIG environment variable..."
# Activate virtual environment if it exists
if [ -d ".venv" ]; then
source .venv/bin/activate
echo "Virtual environment activated"
else
echo "Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
fi
# Set environment variable and install packages
export MARIADB_CONFIG="$MARIADB_CONFIG_PATH"
pip install -r requirements.txt
echo ""
echo "Setup complete! To activate the environment in the future:"
echo " source .venv/bin/activate"
echo " export MARIADB_CONFIG=\"$MARIADB_CONFIG_PATH\""

107
sql/INSTALL_PROCEDURES.md Normal file
View File

@@ -0,0 +1,107 @@
# Installing Stored Procedures
All stored procedure SQL files are ready to use. You have several options:
## Prerequisites
1. **SSH Port Forwarding** - Make sure you have an active SSH tunnel:
```bash
ssh -L 3306:localhost:3306 lukesau.com
```
Keep this terminal open while running SQL commands.
2. **MySQL Client** - Install if needed:
```bash
brew install mysql-client
```
To add mysql-client to your PATH permanently, add to `~/.zshrc`:
```bash
echo 'export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
## Option 1: Use the Helper Script (Easiest)
```bash
./sql/run_sql.sh sql/create_all_stored_procedures.sql
```
## Option 2: Use MySQL Client Directly
If mysql is in your PATH (added to ~/.zshrc), you can use:
```bash
mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/create_all_stored_procedures.sql
```
Or use the full path:
```bash
/opt/homebrew/opt/mysql-client/bin/mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/create_all_stored_procedures.sql
```
**Note:** The `-h 127.0.0.1 -P 3306` flags connect through your SSH tunnel to the remote database.
## Option 3: Interactive MariaDB Session
If you're already logged into MariaDB on the server:
```sql
source sql/create_all_stored_procedures.sql;
```
## Option 4: Install Individually
Run each procedure file separately using the helper script:
```bash
./sql/run_sql.sh sql/select_base1_citizens_sp.sql
./sql/run_sql.sh sql/select_base1_monsters_sp.sql
./sql/run_sql.sh sql/select_base2_citizens_sp.sql
./sql/run_sql.sh sql/select_base2_monsters_sp.sql
./sql/run_sql.sh sql/select_random_domains_sp.sql
./sql/run_sql.sh sql/select_random_dukes_sp.sql
```
Or using mysql client directly:
```bash
/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < sql/select_base1_citizens_sp.sql
# ... repeat for each file
```
Or interactively in MariaDB on the server:
```sql
source sql/select_base1_citizens_sp.sql;
source sql/select_base1_monsters_sp.sql;
source sql/select_base2_citizens_sp.sql;
source sql/select_base2_monsters_sp.sql;
source sql/select_random_domains_sp.sql;
source sql/select_random_dukes_sp.sql;
```
## Verify Installation
After installing, verify with:
```sql
SHOW PROCEDURE STATUS WHERE Db = 'vckonline';
```
Or run the test script:
```bash
python3 test_database.py
```
## What Each Procedure Does
- **select_base1_citizens()** - Returns all citizens from base game 1
- **select_base1_monsters()** - Returns all monsters from base game 1
- **select_base2_citizens()** - Returns base game 2 citizens + Peasant and Knight from base1
- **select_base2_monsters()** - Returns base game 2 monsters + 2 random areas from base1
- **select_random_domains()** - Returns 15 random domains
- **select_random_dukes()** - Returns all dukes in random order

129
sql/USER_SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,129 @@
# MariaDB User Setup Guide for VCK Online
Run these commands interactively in MariaDB as root to investigate and fix the user setup.
## Investigation Commands
### 1. Check if the database exists
```sql
SHOW DATABASES LIKE 'vckonline';
```
### 2. Check if the user exists and from which hosts
```sql
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
```
### 3. Check current privileges (if user exists)
```sql
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'127.0.0.1';
SHOW GRANTS FOR 'vckonline'@'%';
```
### 4. Check if database has data (if database exists)
```sql
USE vckonline;
SHOW TABLES;
```
### 5. Verify data exists in key tables
```sql
SELECT COUNT(*) AS citizens_count FROM citizens;
SELECT COUNT(*) AS monsters_count FROM monsters;
SELECT COUNT(*) AS domains_count FROM domains;
SELECT COUNT(*) AS dukes_count FROM dukes;
SELECT COUNT(*) AS starters_count FROM starters;
```
## Fix Commands
### Scenario A: User doesn't exist - Create new user
```sql
-- Create user for localhost connections
CREATE USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- Create user for 127.0.0.1 connections (SSH tunnel)
CREATE USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- Create user for remote connections (optional, for direct connections)
CREATE USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant privileges
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
### Scenario B: User exists but password is wrong - Reset password
```sql
-- Reset password for existing user
ALTER USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
ALTER USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
ALTER USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Ensure privileges are granted
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
### Scenario C: User exists but lacks privileges - Grant privileges
```sql
-- Grant privileges
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
```
## Verification
After running the fix commands, verify the setup:
```sql
-- Check user exists
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
-- Check privileges
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'127.0.0.1';
-- Test connection (from another terminal, not in MariaDB)
-- mysql -u vckonline -p vckonline
-- Password: vckonline
```
## Quick Fix (All-in-One)
If you're confident the database exists with data, run this complete setup:
```sql
-- Create users if they don't exist (will error if they do, that's OK)
CREATE USER IF NOT EXISTS 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
CREATE USER IF NOT EXISTS 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
CREATE USER IF NOT EXISTS 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant privileges (safe to run even if already granted)
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Apply changes
FLUSH PRIVILEGES;
-- Verify
SHOW GRANTS FOR 'vckonline'@'localhost';
```

View File

@@ -1,6 +1,6 @@
INSERT INTO vckonline.citizens (name,gold_cost,roll_match1,roll_match2,shadow_count,holy_count,soldier_count,worker_count,gold_payout_on_turn,gold_payout_off_turn,strength_payout_on_turn,strength_payout_off_turn,magic_payout_on_turn,magic_payout_off_turn,has_special_payout_on_turn,has_special_payout_off_turn,special_payout_on_turn,special_payout_off_turn,special_citizen,image) VALUES
('Cleric',3,1,0,0,1,0,0,0,0,0,0,3,1,0,0,NULL,NULL,0,NULL),
('Merchant',2,2,0,0,0,0,1,0,1,0,0,0,0,1,0,NULL,NULL,0,NULL),
('Merchant',2,2,0,0,0,0,1,1,0,0,0,0,0,1,0,NULL,NULL,0,NULL),
('Mercenary',3,3,0,1,0,0,0,1,0,1,0,0,0,0,1,NULL,NULL,0,NULL),
('Archer',4,4,0,0,0,1,0,0,0,2,1,0,0,0,0,NULL,NULL,0,NULL),
('Peasant',2,5,0,0,0,0,1,1,1,0,0,0,0,0,0,NULL,NULL,0,NULL),

View File

@@ -0,0 +1,76 @@
-- Create all stored procedures for VCK Online
-- Run this file as the vckonline user (or any user with CREATE ROUTINE privilege on vckonline database)
-- Usage: mysql -u vckonline -p vckonline < create_all_stored_procedures.sql
-- Or interactively: source create_all_stored_procedures.sql;
DELIMITER //
-- Drop existing procedures if they exist (to allow re-running this script)
DROP PROCEDURE IF EXISTS select_base1_citizens //
DROP PROCEDURE IF EXISTS select_base1_monsters //
DROP PROCEDURE IF EXISTS select_base2_citizens //
DROP PROCEDURE IF EXISTS select_base2_monsters //
DROP PROCEDURE IF EXISTS select_random_domains //
DROP PROCEDURE IF EXISTS select_random_dukes //
-- Base 1 Citizens
CREATE PROCEDURE select_base1_citizens()
BEGIN
SELECT * FROM citizens WHERE expansion = "base1";
END //
-- Base 1 Monsters
CREATE PROCEDURE select_base1_monsters()
BEGIN
SELECT * FROM monsters WHERE expansion = "base1";
END //
-- Base 2 Citizens
CREATE PROCEDURE select_base2_citizens()
BEGIN
SELECT * FROM citizens WHERE expansion = "base2"
UNION
SELECT * FROM citizens WHERE expansion = "base1" AND name IN ('Peasant', 'Knight');
END //
-- Base 2 Monsters
CREATE PROCEDURE select_base2_monsters()
BEGIN
DECLARE chosen_area1 VARCHAR(255);
DECLARE chosen_area2 VARCHAR(255);
SET chosen_area1 = (
SELECT area FROM monsters WHERE expansion = 'base1' GROUP BY area ORDER BY RAND() LIMIT 1
);
SET chosen_area2 = (
SELECT area FROM monsters WHERE expansion = 'base1' AND area <> chosen_area1 ORDER BY RAND() LIMIT 1
);
SELECT id_monsters, name, area, monster_type, monster_order,
strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward,
has_special_reward, special_reward, has_special_cost, special_cost, is_extra, expansion
FROM monsters
WHERE expansion = 'base2'
UNION
SELECT id_monsters, name, area, monster_type, monster_order,
strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward,
has_special_reward, special_reward, has_special_cost, special_cost, is_extra, expansion
FROM monsters
WHERE expansion = 'base1' AND area IN (chosen_area1, chosen_area2);
END //
-- Random Domains
CREATE PROCEDURE select_random_domains()
BEGIN
SELECT * FROM domains ORDER BY RAND() LIMIT 15;
END //
-- Random Dukes
CREATE PROCEDURE select_random_dukes()
BEGIN
SELECT * FROM dukes ORDER BY RAND();
END //
DELIMITER ;
-- Verify procedures were created
SHOW PROCEDURE STATUS WHERE Db = 'vckonline';

View File

@@ -1,17 +1,17 @@
INSERT INTO vckonline.domains (name,gold_cost,shadow_count,holy_count,soldier_count,worker_count,vp_reward,has_activation_effect,has_passive_effect,passive_effect,activation_effect,`text`,image) VALUES
('Jousting Field',13,1,0,1,1,3,0,1,NULL,NULL,'During your Harvest Phase, gain 1gp * Knight you own.',NULL),
('Ancient Tomb',7,0,1,1,1,3,1,0,NULL,NULL,'Immediately add 3sp to a Monster Strength value.',NULL),
('Foxgrove Palisade',9,1,0,1,0,3,0,1,NULL,NULL,'During your Roll Phase, you may pay 2gp to change one die to equal 6.',NULL),
('The Desert Orchid',9,1,1,0,0,3,0,1,NULL,NULL,'During your Roll Phase, you may pay 1gp * owned Holy Citizens to change one die to equal 1.',NULL),
('Pretorius Conclave',8,1,1,1,1,2,1,0,NULL,NULL,'Immediately gain a Citizen from the center stacks.',NULL),
('Emerald Stronghold',12,0,1,1,1,5,0,1,NULL,NULL,'During your Action phase, ignore ''+'' when buying Citizens.',NULL),
('Pratchett''s Plateau',8,0,1,0,1,3,0,1,NULL,NULL,'During your Action phase, Domains cost you 1gp less to buy.',NULL),
('Shelly Commons',13,0,1,1,1,4,0,1,NULL,NULL,'At the end of your Action phase, pay 1gp to a Player of your choice to gain 1vp.',NULL),
('Cathedral of St Aquila',8,0,2,0,0,3,0,1,NULL,NULL,'At the end of your Action phase, take 1gp from a Player of your choice.',NULL),
('Cursed Cavern',10,1,1,0,1,2,1,0,NULL,NULL,'All players immediately flip a Citizen and you gain 4mp.',NULL);
('Jousting Field',13,1,0,1,1,3,0,1,'harvest:gain_per_owned_citizen_name Knight g 1',NULL,'During your Harvest Phase, gain 1gp * Knight you own.',NULL),
('Ancient Tomb',7,0,1,1,1,3,1,0,NULL,'activation_monster_strength +3','Immediately add 3sp to a Monster Strength value.',NULL),
('Foxgrove Palisade',9,1,0,1,0,3,0,1,'roll:set_one_die target=6 cost=g:2',NULL,'During your Roll Phase, you may pay 2gp to change one die to equal 6.',NULL),
('The Desert Orchid',9,1,1,0,0,3,0,1,'roll:set_one_die target=1 cost=g_per_owned_role:holy_citizen',NULL,'During your Roll Phase, you may pay 1gp * owned Holy Citizens to change one die to equal 1.',NULL),
('Pretorius Conclave',8,1,1,1,1,2,1,0,NULL,'choose <citizens>','Immediately gain a Citizen from the center stacks.',NULL),
('Emerald Stronghold',12,0,1,1,1,5,0,1,'effect:add action:emeraldstronghold',NULL,'During your Action phase, ignore ''+'' when buying Citizens.',NULL),
('Pratchett''s Plateau',8,0,1,0,1,3,0,1,'effect:add action:pratchettsplateau',NULL,'During your Action phase, Domains cost you 1gp less to build.',NULL),
('Shelley Commons',13,0,1,1,1,4,0,1,'action:end manipulate_resources mode=pay_to_player gain=vp:1 pay=g:1 optional=true',NULL,'At the end of your Action phase, pay 1gp to a Player of your choice to gain 1vp.',NULL),
('Cathedral of St Aquila',8,0,2,0,0,3,0,1,'action:end manipulate_resources mode=take_from_player take=g:1 optional=true',NULL,'At the end of your Action phase, take 1gp from a Player of your choice.',NULL),
('Cursed Cavern',10,1,1,0,1,2,1,0,NULL,'m 4 + concurrent_flip_one_citizen','All players immediately flip a Citizen and you gain 4mp.',NULL);
INSERT INTO vckonline.domains (name,gold_cost,shadow_count,holy_count,soldier_count,worker_count,vp_reward,has_activation_effect,has_passive_effect,passive_effect,activation_effect,`text`,image) VALUES
('Darktide Harbour',6,1,0,1,1,2,1,0,NULL,NULL,'Immediately gain a Shadow Citizen from the center stacks.',NULL),
('King Tower',12,0,1,0,2,3,0,1,NULL,NULL,'At the end of your Action Phase, pay 1mp to a Player of your choice to gain 1vp.',NULL),
('Cloudrider''s Camp',8,0,1,1,0,2,1,0,NULL,NULL,'Immediately gain 3sp and a Soldier Citizen worth 2gp or less.',NULL),
('The Orb of Urdr',6,1,1,0,0,1,0,1,NULL,NULL,'At the end of your Action Phase, take 1mp from a Player of your choice.',NULL),
('Wisborg',6,1,0,0,2,3,1,0,NULL,NULL,'You may immediately pay 3gp to gain 3vp.',NULL);
('Darktide Harbour',6,1,0,1,1,2,1,0,NULL,'choose <citizens where role==shadow>','Immediately gain a Shadow Citizen from the center stacks.',NULL),
('King Tower',12,0,1,0,2,3,0,1,'action:end manipulate_resources mode=pay_to_player gain=vp:1 pay=m:1 optional=true',NULL,'At the end of your Action Phase, pay 1mp to a Player of your choice to gain 1vp.',NULL),
('Cloudrider''s Camp',8,0,1,1,0,2,1,0,NULL,'s 3 + choose <citizens where role==soldier and gold_cost<=2>','Immediately gain 3sp and a Soldier Citizen worth 2gp or less.',NULL),
('The Orb of Urdr',6,1,1,0,0,1,0,1,'action:end manipulate_resources mode=take_from_player take=m:1 optional=true',NULL,'At the end of your Action Phase, take 1mp from a Player of your choice.',NULL),
('Wisborg',6,1,0,0,2,3,1,0,NULL,'manipulate_resources mode=self_convert pay=g:3 gain=vp:3 optional=true','You may immediately pay 3gp to gain 3vp.',NULL);

56
sql/fix_user_setup.sql Normal file
View File

@@ -0,0 +1,56 @@
-- ============================================
-- VCK Online Database User Setup Commands
-- Run these interactively in MariaDB as root
-- ============================================
-- Step 1: Check if the database exists
SHOW DATABASES LIKE 'vckonline';
-- Step 2: Check if the user exists
SELECT User, Host FROM mysql.user WHERE User = 'vckonline';
-- Step 3: Check current user privileges (if user exists)
SHOW GRANTS FOR 'vckonline'@'localhost';
SHOW GRANTS FOR 'vckonline'@'%';
-- Step 4: Check what tables exist in the database (if it exists)
USE vckonline;
SHOW TABLES;
-- Step 5: Check table row counts to verify data exists
SELECT
'citizens' AS table_name, COUNT(*) AS row_count FROM citizens
UNION ALL
SELECT 'monsters', COUNT(*) FROM monsters
UNION ALL
SELECT 'domains', COUNT(*) FROM domains
UNION ALL
SELECT 'dukes', COUNT(*) FROM dukes
UNION ALL
SELECT 'starters', COUNT(*) FROM starters;
-- ============================================
-- FIX COMMANDS (run only if needed)
-- ============================================
-- Option A: If user doesn't exist, create it
-- CREATE USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- CREATE USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- CREATE USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Option B: If user exists but password is wrong, reset it
-- ALTER USER 'vckonline'@'localhost' IDENTIFIED BY 'vckonline';
-- ALTER USER 'vckonline'@'127.0.0.1' IDENTIFIED BY 'vckonline';
-- ALTER USER 'vckonline'@'%' IDENTIFIED BY 'vckonline';
-- Grant all privileges on vckonline database
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'localhost';
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'127.0.0.1';
-- GRANT ALL PRIVILEGES ON vckonline.* TO 'vckonline'@'%';
-- Flush privileges to apply changes
-- FLUSH PRIVILEGES;
-- Verify the grants after creating/fixing
-- SHOW GRANTS FOR 'vckonline'@'localhost';

63
sql/run_sql.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Helper script to run SQL files on remote database through SSH port forwarding
# Usage: ./sql/run_sql.sh sql/your_file.sql
# Or: bash sql/run_sql.sh sql/your_file.sql
# Find mysql binary (try PATH first, then specific location)
if command -v mysql >/dev/null 2>&1; then
MYSQL_BIN="mysql"
elif [ -f "/opt/homebrew/opt/mysql-client/bin/mysql" ]; then
MYSQL_BIN="/opt/homebrew/opt/mysql-client/bin/mysql"
elif [ -f "/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql" ]; then
MYSQL_BIN="/opt/homebrew/Cellar/mysql-client/9.5.0/bin/mysql"
else
echo "Error: mysql binary not found"
echo "Please install mysql-client: brew install mysql-client"
echo "Or add to PATH: export PATH=\"/opt/homebrew/opt/mysql-client/bin:\$PATH\""
exit 1
fi
# Check if SQL file is provided
if [ -z "$1" ]; then
echo "Usage: $0 <sql_file>"
echo "Example: $0 sql/create_all_stored_procedures.sql"
exit 1
fi
SQL_FILE="$1"
# Check if SQL file exists
if [ ! -f "$SQL_FILE" ]; then
echo "Error: SQL file not found: $SQL_FILE"
exit 1
fi
# Check if port 3306 is accessible (SSH tunnel check)
if ! nc -z 127.0.0.1 3306 2>/dev/null; then
echo "Warning: Cannot connect to localhost:3306"
echo "Make sure SSH port forwarding is active:"
echo " ssh -L 3306:localhost:3306 lukesau.com"
echo ""
read -p "Continue anyway? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Run the SQL file
echo "Running SQL file: $SQL_FILE"
echo "Connecting to database through SSH tunnel (localhost:3306)..."
echo ""
"$MYSQL_BIN" -h 127.0.0.1 -P 3306 -u vckonline -p vckonline < "$SQL_FILE"
if [ $? -eq 0 ]; then
echo ""
echo "✓ SQL file executed successfully"
else
echo ""
echo "✗ Error executing SQL file"
exit 1
fi

View File

@@ -0,0 +1,213 @@
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
.section { border: 1px solid #ccc; padding: 15px; margin: 10px 0; }
button { padding: 8px 15px; margin: 5px; cursor: pointer; }
input { padding: 5px; margin: 5px; }
.lobby-player { padding: 5px; margin: 2px; background: #f0f0f0; }
.ready { background: #90EE90; }
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
details { margin-top: 10px; }
details > summary { cursor: pointer; font-weight: 700; user-select: none; }
.dice-row { margin: 10px 0; }
.dice { display: flex; gap: 10px; align-items: center; }
.dice-panel {
margin-top: 8px;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 10px;
background: #fff;
}
.dice-panel-layout {
display: grid;
grid-template-columns: auto minmax(260px, 1fr) auto;
gap: 14px;
align-items: start;
}
.dice-panel-col { min-width: 0; }
.dice-panel label { font-size: 13px; }
.dice-panel-fields { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 6px; align-items: center; }
.dice-panel-fields input[type="number"] { width: 70px; }
.dice-panel-hint { margin-top: 6px; font-size: 12px; color: #444; }
.roll-effects { margin-top: 8px; font-size: 12px; color: #333; }
.roll-effects ul { margin: 4px 0 0 18px; padding: 0; }
@media (max-width: 980px) {
.dice-panel-layout { grid-template-columns: 1fr; }
}
.die {
width: 44px; height: 44px;
border: 2px solid #222;
border-radius: 10px;
background: #fff;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
padding: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.die.increase { border-color: #0a6; color: #0a6; }
.die.decrease { border-color: #b00; color: #b00; }
.pip { width: 8px; height: 8px; border-radius: 50%; background: #111; justify-self: center; align-self: center; }
.die.increase .pip { background: currentColor; }
.die.decrease .pip { background: currentColor; }
.pip.off { opacity: 0; }
.dice-meta { color: #333; font-size: 14px; }
.delta-wrap { display: flex; flex-wrap: wrap; gap: 10px; }
.delta-card {
border: 1px solid #ddd;
background: #fafafa;
border-radius: 8px;
padding: 6px 10px;
font-size: 13px;
color: #222;
}
.delta-grid {
display: grid;
grid-template-columns: minmax(110px, 1fr) repeat(4, 64px);
column-gap: 10px;
row-gap: 2px;
align-items: baseline;
}
.delta-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.delta-cell { display: grid; grid-template-columns: auto 1fr; column-gap: 6px; }
.delta-label { color: #666; font-weight: 700; }
.delta-value { text-align: right; font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.delta-pos { color: #0a6; font-weight: 700; }
.delta-neg { color: #b00; font-weight: 700; }
.delta-zero { color: #666; font-weight: 700; }
.delta-totals { color: #111; font-weight: 700; }
.delta-muted { color: #666; font-weight: 600; }
/* Tableau modal (dev UI) */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: none;
z-index: 9999;
padding: 30px;
}
.modal-backdrop.open { display: block; }
.modal-panel {
background: #fff;
border-radius: 12px;
max-width: 980px;
margin: 0 auto;
max-height: calc(100vh - 60px);
overflow: auto;
border: 1px solid rgba(0,0,0,0.15);
box-shadow: 0 18px 60px rgba(0,0,0,0.35);
}
.modal-header {
position: sticky;
top: 0;
background: #fff;
border-bottom: 1px solid #eee;
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.modal-title { font-weight: 800; }
.modal-close {
border: 1px solid #ddd;
background: #fafafa;
border-radius: 10px;
padding: 6px 10px;
cursor: pointer;
font-weight: 700;
}
.modal-body { padding: 14px; }
.kv { display: flex; gap: 8px; flex-wrap: wrap; margin: 6px 0 12px; }
.pill {
border: 1px solid #e2e2e2;
background: #f8f8f8;
border-radius: 999px;
padding: 4px 10px;
font-size: 13px;
color: #222;
}
.tableau-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 860px) { .tableau-grid { grid-template-columns: 1fr; } }
.tableau-card {
border: 1px solid #e6e6e6;
border-radius: 10px;
background: #fff;
padding: 10px;
}
.tableau-card h3 { margin: 0 0 8px 0; font-size: 16px; }
.mini { font-size: 13px; color: #444; }
.list { display: flex; flex-direction: column; gap: 6px; }
.item {
border: 1px solid #eee;
background: #fafafa;
border-radius: 8px;
padding: 8px 10px;
}
.item-title { font-weight: 800; }
.item-sub { color: #555; font-size: 13px; margin-top: 3px; }
/* Tableau seat buttons (around Board) */
.tableau-actions { margin-top: 10px; }
.tableau-seat-layout {
position: relative;
width: 100%;
max-width: 760px;
height: 220px;
margin-top: 8px;
border: 1px solid #e6e6e6;
border-radius: 12px;
background: #fff;
}
@media (max-width: 560px) { .tableau-seat-layout { height: 260px; } }
.tableau-seat-btn {
position: absolute;
transform: translate(-50%, -50%);
white-space: nowrap;
border: 1px solid #ddd;
background: #fafafa;
border-radius: 10px;
padding: 6px 10px;
cursor: pointer;
font-weight: 700;
}
.tableau-seat-btn.first-seat {
border-color: #b7a200;
background: #fff8cc;
}
.tableau-seat-btn.board-seat {
background: #111;
border-color: #111;
color: #fff;
}
/* Payment editor (hire / build / slay) */
.pay-row { display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap; }
.cost-line { flex: 1; min-width: 200px; }
.pay-controls { display: block; margin-top: 4px; }
.pay-controls input[type="number"] { width: 58px; }
.game-log-wrap {
margin-top: 14px;
border: 1px solid #ccc;
border-radius: 8px;
background: #fafafa;
overflow: hidden;
}
.game-log-wrap h3 {
margin: 0;
padding: 8px 10px;
font-size: 14px;
background: #eee;
border-bottom: 1px solid #ddd;
}
#gameLog {
max-height: 220px;
overflow-y: auto;
padding: 8px 10px;
font-size: 12px;
line-height: 1.45;
font-family: ui-monospace, Menlo, Monaco, "Courier New", monospace;
color: #222;
}
.game-log-line { margin: 2px 0; }
.game-log-tick { color: #666; margin-right: 6px; user-select: none; }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html>
<head>
<title>VCK Online - Dev Client</title>
<link rel="stylesheet" href="/static/dev-client/dev-client.css">
</head>
<body>
<h1>VCK Online - Development Client</h1>
<div class="section">
<h2>Lobby</h2>
<div>
<input type="text" id="playerName" placeholder="Enter your name">
<button onclick="joinLobby()">Join Lobby</button>
<button onclick="getLobbyStatus()">Refresh</button>
</div>
<div style="margin-top: 8px;">
<label>
<input type="checkbox" id="debugStartingResourcesEnabled">
Debug start resources (100 gold / 100 strength / 100 magic)
</label>
</div>
<div id="lobbyStatus"></div>
<div id="playerId" style="margin-top: 10px; font-weight: bold;"></div>
</div>
<div class="section">
<h2>Game</h2>
<div id="gameStatus"></div>
<div class="dice-row">
<div class="dice-meta" id="diceMeta"></div>
<div class="dice-panel" id="dicePanel">
<div class="dice-panel-layout">
<div class="dice-panel-col">
<div class="dice" id="dice"></div>
</div>
<div class="dice-panel-col">
<div class="roll-effects" id="rollEffects"></div>
</div>
<div class="dice-panel-col">
<label>
<input type="checkbox" id="diceOverrideEnabled">
Use custom dice finalize (dev)
</label>
<div class="dice-panel-fields">
<label>Die 1
<input type="number" id="diceOverrideDie1" min="1" max="6" step="1">
</label>
<label>Die 2
<input type="number" id="diceOverrideDie2" min="1" max="6" step="1">
</label>
</div>
<div class="dice-panel-hint" id="dicePanelHint"></div>
</div>
</div>
</div>
<div class="delta-wrap" id="harvestDeltas" style="margin-top: 6px;"></div>
<div style="margin-top: 8px;">
<label>
<input type="checkbox" id="autoHarvestEnabled">
Auto-harvest single-option resource prompts
</label>
</div>
<div id="choicePanel" style="margin-top: 8px;"></div>
</div>
<div class="game-log-wrap">
<h3>Game log</h3>
<div id="gameLog" aria-live="polite"></div>
</div>
<button onclick="getGameState()">Refresh Game State</button>
<div class="tableau-actions">
<div class="mini"><strong>Tableau seats:</strong> buttons are arranged in turn order around the Board.</div>
<div id="tableauSeatLayout" class="tableau-seat-layout" aria-label="Tableau seats"></div>
</div>
<details>
<summary>Game state JSON</summary>
<pre id="gameState"></pre>
</details>
</div>
<div id="tableauModal" class="modal-backdrop" onclick="onTableauBackdropClick(event)">
<div class="modal-panel" onclick="event.stopPropagation()">
<div class="modal-header">
<div class="modal-title" id="tableauTitle">My Tableau</div>
<button class="modal-close" onclick="closeTableau()">Close</button>
</div>
<div class="modal-body" id="tableauBody"></div>
</div>
</div>
<script src="/static/dev-client/dev-client.js" defer></script>
</body>
</html>

359
test_database.py Normal file
View File

@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""
Test script to check MariaDB database connection and status for VCK Online
"""
import sys
def test_database_connection():
"""Test the database connection and basic functionality"""
# Database connection parameters from game.py
# Using localhost for SSH port forwarding (ssh -L 3306:localhost:3306 lukesau.com)
db_config = {
'user': 'vckonline',
'password': 'vckonline',
'host': '127.0.0.1',
'database': 'vckonline'
}
print("Testing VCK Online Database Connection")
print("=" * 50)
# Test 1: Check if mariadb module is available
print("\n1. Checking mariadb module...")
try:
import mariadb
print(" ✓ mariadb module found")
except ImportError:
print(" ✗ mariadb module not found")
print(" Install with: pip install mariadb")
print(" Or with user flag: pip install --user mariadb")
print(" Or use virtualenv: python3 -m venv .env && source .env/bin/activate && pip install mariadb")
return False
# Test 1.5: Check if database server is accessible
print("\n1.5. Checking if database server is accessible on localhost:3306...")
print(" (Make sure SSH port forwarding is active: ssh -L 3306:localhost:3306 lukesau.com)")
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('127.0.0.1', 3306))
sock.close()
if result == 0:
print(" ✓ Database server is accessible on localhost:3306")
else:
print(" ✗ Cannot reach localhost:3306")
print("\n Make sure SSH port forwarding is active:")
print(" ssh -L 3306:localhost:3306 lukesau.com")
return False
# Test 2: Test connection
print("\n2. Testing database connection...")
try:
connection = mariadb.connect(**db_config)
print(f" ✓ Successfully connected to database '{db_config['database']}'")
cursor = connection.cursor(dictionary=True)
except mariadb.Error as e:
print(f" ✗ Connection failed: {e}")
print("\n Possible issues:")
print(" - Database 'vckonline' does not exist")
print(" - User 'vckonline' does not exist or password is incorrect")
print(" - User does not have permission to access from this IP")
return False
# Test 3: Check if required tables exist
print("\n3. Checking required tables...")
required_tables = ['citizens', 'monsters', 'domains', 'dukes', 'starters']
existing_tables = []
try:
cursor.execute("SHOW TABLES")
# SHOW TABLES returns a dict with key like 'Tables_in_vckonline'
tables = [list(row.values())[0] for row in cursor.fetchall()]
for table in required_tables:
if table in tables:
print(f" ✓ Table '{table}' exists")
existing_tables.append(table)
else:
print(f" ✗ Table '{table}' is missing")
except mariadb.Error as e:
print(f" ✗ Error checking tables: {e}")
connection.close()
return False
# Test 4: Check table row counts
print("\n4. Checking table data...")
for table in existing_tables:
try:
cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
count = cursor.fetchone()['count']
print(f" {table}: {count} rows")
except mariadb.Error as e:
print(f" ✗ Error counting rows in '{table}': {e}")
# Test 4.5: Display all card data
print("\n4.5. Displaying all card data...")
# Citizens
try:
cursor.execute("SELECT name, gold_cost, roll_match1, roll_match2, shadow_count, holy_count, soldier_count, worker_count, expansion FROM citizens ORDER BY expansion, roll_match1")
citizens = cursor.fetchall()
print(f"\n CITIZENS ({len(citizens)} total):")
for c in citizens:
name, gc, r1, r2, sh, ho, so, wo, exp = c
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
roll_str = f"{r1}" + (f"/{r2}" if r2 else "")
print(f" {name:20} | Cost: {gc:2}gp | Roll: {roll_str:5} | {role_str:20} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching citizens: {e}")
# Monsters
try:
cursor.execute("SELECT name, area, monster_type, monster_order, strength_cost, magic_cost, vp_reward, gold_reward, strength_reward, magic_reward, expansion FROM monsters ORDER BY area, monster_order")
monsters = cursor.fetchall()
print(f"\n MONSTERS ({len(monsters)} total):")
for m in monsters:
name, area, mtype, order, sc, mc, vp, gr, sr, mr, exp = m
cost_str = f"{sc}sp" + (f" + {mc}mp" if mc else "")
reward_str = f"{vp}vp" + (f" + {gr}gp" if gr else "") + (f" + {sr}sp" if sr else "") + (f" + {mr}mp" if mr else "")
print(f" {name:25} | {area:10} | {mtype:8} | Cost: {cost_str:10} | Reward: {reward_str:15} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching monsters: {e}")
# Domains
try:
cursor.execute("SELECT name, gold_cost, shadow_count, holy_count, soldier_count, worker_count, vp_reward, text, expansion FROM domains ORDER BY expansion, gold_cost")
domains = cursor.fetchall()
print(f"\n DOMAINS ({len(domains)} total):")
for d in domains:
name, gc, sh, ho, so, wo, vp, text, exp = d
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
text_preview = (text[:40] + "...") if text and len(text) > 40 else (text or "No effect")
print(f" {name:25} | Cost: {gc:2}gp | {role_str:20} | {vp}vp | {text_preview} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching domains: {e}")
# Dukes
try:
cursor.execute("SELECT name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult, worker_mult, monster_mult, citizen_mult, domain_mult, expansion FROM dukes ORDER BY expansion, name")
dukes = cursor.fetchall()
print(f"\n DUKES ({len(dukes)} total):")
for d in dukes:
name, gm, sm, mm, shm, hom, som, wom, mom, cm, dom, exp = d
mults = []
if gm: mults.append(f"Gold×{gm}")
if sm: mults.append(f"Str×{sm}")
if mm: mults.append(f"Mag×{mm}")
if shm: mults.append(f"Shadow×{shm}")
if hom: mults.append(f"Holy×{hom}")
if som: mults.append(f"Soldier×{som}")
if wom: mults.append(f"Worker×{wom}")
if mom: mults.append(f"Monster×{mom}")
if cm: mults.append(f"Citizen×{cm}")
if dom: mults.append(f"Domain×{dom}")
mult_str = ", ".join(mults) if mults else "No multipliers"
print(f" {name:30} | {mult_str} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching dukes: {e}")
# Starters
try:
cursor.execute("SELECT name, roll_match1, roll_match2, gold_payout_on_turn, gold_payout_off_turn, strength_payout_on_turn, strength_payout_off_turn, magic_payout_on_turn, magic_payout_off_turn, expansion FROM starters ORDER BY roll_match1")
starters = cursor.fetchall()
print(f"\n STARTERS ({len(starters)} total):")
for s in starters:
name, r1, r2, gpot, gpoff, spot, spoff, mpot, mpoff, exp = s
roll_str = f"{r1}" + (f"/{r2}" if r2 else "")
payouts = []
if gpot: payouts.append(f"{gpot}gp (on)")
if gpoff: payouts.append(f"{gpoff}gp (off)")
if spot: payouts.append(f"{spot}sp (on)")
if spoff: payouts.append(f"{spoff}sp (off)")
if mpot: payouts.append(f"{mpot}mp (on)")
if mpoff: payouts.append(f"{mpoff}mp (off)")
payout_str = ", ".join(payouts) if payouts else "No payouts"
print(f" {name:20} | Roll: {roll_str:5} | {payout_str} | {exp}")
except mariadb.Error as e:
print(f" ✗ Error fetching starters: {e}")
# Test 5: Test stored procedures
print("\n5. Checking stored procedures...")
print(" (These are helper functions used by the game code to select cards)")
required_procedures = [
'select_base1_citizens',
'select_base1_monsters',
'select_base2_citizens',
'select_base2_monsters',
'select_random_domains',
'select_random_dukes'
]
try:
cursor.execute("SHOW PROCEDURE STATUS WHERE Db = 'vckonline'")
procedures = [row['Name'] for row in cursor.fetchall()]
missing_procedures = []
for proc in required_procedures:
if proc in procedures:
print(f" ✓ Procedure '{proc}' exists")
else:
print(f" ✗ Procedure '{proc}' is missing")
missing_procedures.append(proc)
if missing_procedures:
print(f"\n Note: {len(missing_procedures)} stored procedures are missing.")
print(" These can be created from the SQL files in the sql/ directory:")
print(" - select_base1_citizens_sp.sql")
print(" - select_base1_monsters_sp.sql")
print(" - select_base2_citizens_sp.sql")
print(" - select_base2_monsters_sp.sql")
print(" - select_random_domains_sp.sql")
print(" - select_random_dukes_sp.sql")
except mariadb.Error as e:
print(f" ✗ Error checking procedures: {e}")
# Test 6: Test a sample query
print("\n6. Testing sample query...")
try:
cursor.execute("SELECT * FROM starters LIMIT 1")
result = cursor.fetchone()
if result:
name = result.get('name', 'N/A')
print(f" ✓ Sample query successful (found starter: {name})")
else:
print(" ⚠ Sample query returned no results (table may be empty)")
except mariadb.Error as e:
print(f" ✗ Sample query failed: {e}")
# Test 7: Test all stored procedures and display results
print("\n7. Testing stored procedures and displaying results...")
# Get list of available procedures
try:
cursor.execute("SHOW PROCEDURE STATUS WHERE Db = 'vckonline'")
available_procedures = {row['Name']: True for row in cursor.fetchall()}
except mariadb.Error as e:
print(f" ✗ Error checking procedures: {e}")
available_procedures = {}
# Test each procedure
procedure_tests = [
('select_base1_citizens', 'Citizens'),
('select_base1_monsters', 'Monsters'),
('select_base2_citizens', 'Citizens'),
('select_base2_monsters', 'Monsters'),
('select_random_domains', 'Domains'),
('select_random_dukes', 'Dukes')
]
for proc_name, card_type in procedure_tests:
if proc_name not in available_procedures:
print(f"\n{proc_name} - Procedure not found (skipping)")
continue
try:
print(f"\n Testing {proc_name}():")
cursor.callproc(proc_name)
results = cursor.fetchall()
if not results:
print(f" ⚠ No results returned")
continue
print(f" ✓ Returned {len(results)} {card_type.lower()}")
# Display results based on card type (using dictionary access)
if card_type == 'Citizens':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gc = row.get('gold_cost')
r1 = row.get('roll_match1')
r2 = row.get('roll_match2')
sh = row.get('shadow_count', 0) or 0
ho = row.get('holy_count', 0) or 0
so = row.get('soldier_count', 0) or 0
wo = row.get('worker_count', 0) or 0
exp = row.get('expansion')
roles = []
if sh: roles.append(f"{sh} Shadow")
if ho: roles.append(f"{ho} Holy")
if so: roles.append(f"{so} Soldier")
if wo: roles.append(f"{wo} Worker")
role_str = ", ".join(roles) if roles else "No roles"
roll_str = f"{r1}" + (f"/{r2}" if r2 and r2 > 0 else "")
gc_str = f"{gc}gp" if gc is not None else "N/A"
exp_str = f" | {exp}" if exp else ""
print(f" {name:20} | Cost: {gc_str:5} | Roll: {roll_str:5} | {role_str:20}{exp_str}")
elif card_type == 'Monsters':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
area = row.get('area')
mtype = row.get('monster_type')
sc = row.get('strength_cost', 0) or 0
mc = row.get('magic_cost', 0) or 0
vp = row.get('vp_reward', 0) or 0
gr = row.get('gold_reward', 0) or 0
exp = row.get('expansion')
cost_str = f"{sc}sp" + (f" + {mc}mp" if mc else "")
reward_str = f"{vp}vp" + (f" + {gr}gp" if gr else "")
exp_str = f" | {exp}" if exp else ""
print(f" {name:25} | {area:10} | {mtype:8} | Cost: {cost_str:10} | Reward: {reward_str:15}{exp_str}")
elif card_type == 'Domains':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gc = row.get('gold_cost')
vp = row.get('vp_reward')
text = row.get('text')
text_preview = (text[:50] + "...") if text and len(text) > 50 else (text or "No effect")
print(f" {name:25} | Cost: {gc:2}gp | {vp}vp | {text_preview}")
elif card_type == 'Dukes':
print(f" {card_type} returned:")
for row in results:
name = row.get('name', 'N/A')
gm = row.get('gold_mult', 0) or 0
sm = row.get('strength_mult', 0) or 0
mm = row.get('magic_mult', 0) or 0
mults = []
if gm: mults.append(f"Gold×{gm}")
if sm: mults.append(f"Str×{sm}")
if mm: mults.append(f"Mag×{mm}")
mult_str = ", ".join(mults) if mults else "No multipliers"
print(f" {name:30} | {mult_str}")
except mariadb.Error as e:
print(f" ✗ Error calling {proc_name}: {e}")
# Cleanup
cursor.close()
connection.close()
print("\n" + "=" * 50)
print("Database test completed")
return True
if __name__ == "__main__":
success = test_database_connection()
sys.exit(0 if success else 1)

View File

@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocker Client</title>
</head>
<body>
<button onclick="contactServer">Click Here</button>
</body>
<script>
const socket = new WebSocket('ws://localhost:8000');
socket.addEventListener('open', function (event) {
socket.send('Connection Established');
});
socket.addEventListener('message', function (event) {
console.log(event.data);
});
const contactServer = () => {
socket.send("Initialize");
}
</script>
</html>

7
vckonline setup.txt Normal file
View File

@@ -0,0 +1,7 @@
virtualenv --no-site-packages --distribute .env && source .env/bin/activate && pip install -r requirements.txt
dnf install python3-devel mysql-devel
pip install mysql-connector-python
pip install mapping
pip install wxpython

View File

@@ -1,14 +1,23 @@
from common import *
from server import load_game_data
import json
import shortuuid
import uuid
from game import *
player1_id = shortuuid.uuid()
player2_id = shortuuid.uuid()
player1 = Player(player1_id, "Player 1")
player2 = Player(player2_id, "Player 2")
player_list = [player1, player2]
game_id = str(uuid.uuid4())
try:
base1_new_game_state = load_game_data(str(uuid.uuid4()), "base1", player_list)
base1_new_game_state = load_game_data(game_id, "base1", player_list)
game = Game(base1_new_game_state)
game.play_turn()
game.hire_citizen(player1_id, 2, 0, 0)
game.hire_citizen(player2_id, 2, 0, 0)
game.die_one = 2
game.die_two = 5
game.die_sum = 7
game.harvest_phase()
game.act_on_required_action(player1_id, "choose 1")
game_json = json.dumps(game, cls=GameObjectEncoder, indent=2)
with open("game_state.txt", "w") as dump:
dump.write(game_json)