| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- """Slot game workflow helpers.
- This module turns a small set of form choices into the manifest format that the
- existing generation pipeline already understands. It is deterministic on
- purpose: the text-model step can be added later, but the UI should already have a
- stable "define game -> manifest -> generate" path.
- """
- import copy
- import re
- import slot_math
- DEFAULT_STYLE_BY_THEME = {
- "jelly": "cute 3D-rendered translucent jelly creatures, glossy gummy texture, soft subsurface highlights, warm candy-land color palette, mobile slot game asset, centered, clean edges",
- "fruit": "bright juicy fruit slot game, glossy 3D fruit icons, fresh arcade colors, mobile game asset, centered, clean edges",
- "egypt": "golden ancient egypt treasure slot game, polished gems, warm sandstone and turquoise palette, premium mobile game asset, centered, clean edges",
- "pirate": "playful pirate treasure slot game, glossy gold coins, tropical ocean colors, stylized 3D mobile game asset, centered, clean edges",
- "pirate_jelly": "cute 3D pirate candy slot game, translucent jelly pirate mascot, glossy gummy texture, tropical island ocean background, wooden pirate UI frame, treasure chest, bright gold coins, colorful candy letters, high-saturation casual mobile casino art, soft rounded shapes, polished 3D render, clean mobile game asset, centered, clean edges",
- "cyber": "neon cyber arcade slot game, glowing glass panels, electric blue and magenta palette, premium mobile game asset, centered, clean edges",
- }
- THEME_LABELS = {
- "jelly": "果冻糖果",
- "fruit": "水果乐园",
- "egypt": "埃及宝藏",
- "pirate": "海盗金币",
- "pirate_jelly": "海盗糖果",
- "cyber": "霓虹赛博",
- }
- VOLATILITY_RULES = {
- "low": {
- "hitFrequencyFeel": "high",
- "minMatch": 3,
- "payMultiplier": 0.82,
- "bonusChanceLabel": "frequent_small",
- "cascadeMultiplierStep": 1,
- "maxCascadeMultiplier": 4,
- },
- "medium": {
- "hitFrequencyFeel": "medium",
- "minMatch": 4,
- "payMultiplier": 1.0,
- "bonusChanceLabel": "balanced",
- "cascadeMultiplierStep": 1,
- "maxCascadeMultiplier": 6,
- },
- "high": {
- "hitFrequencyFeel": "low",
- "minMatch": 5,
- "payMultiplier": 1.35,
- "bonusChanceLabel": "rare_large",
- "cascadeMultiplierStep": 2,
- "maxCascadeMultiplier": 10,
- },
- }
- FEEDBACK_RULES = {
- "quiet": {
- "spinDurationSec": 0.32,
- "dropTimeSec": 0.24,
- "clearPopScale": 1.18,
- "screenShake": False,
- "bigWinThresholdBet": 18,
- "vfxDensity": "light",
- },
- "standard": {
- "spinDurationSec": 0.45,
- "dropTimeSec": 0.30,
- "clearPopScale": 1.30,
- "screenShake": False,
- "bigWinThresholdBet": 25,
- "vfxDensity": "medium",
- },
- "loud": {
- "spinDurationSec": 0.58,
- "dropTimeSec": 0.36,
- "clearPopScale": 1.42,
- "screenShake": True,
- "bigWinThresholdBet": 35,
- "vfxDensity": "heavy",
- },
- }
- BASE_SYMBOLS = {
- "jelly": [
- ("jelly_blue", "a round blue blueberry jelly creature mascot with big friendly eyes and a happy open smile, tiny arms raised"),
- ("jelly_pink", "a pink strawberry jelly creature with sparkling eyes and a small leaf on top, cheerful pose"),
- ("jelly_green", "a green apple jelly creature with rosy cheeks and a cute grin"),
- ("jelly_orange", "an orange citrus jelly creature with bright eyes and little stubby hands"),
- ("jelly_purple", "a purple grape jelly creature, slightly wobbly, shy smile, sparkles"),
- ("jelly_lemon", "a yellow lemon jelly creature with a wide excited smile and star-shaped eyes"),
- ("jelly_choco", "a glossy chocolate jelly creature with caramel swirls and warm happy eyes"),
- ("jelly_rainbow", "a rainbow gradient jelly creature, extra glossy and translucent, joyful expression, premium special character"),
- ("symbol_coin", "a shiny golden coin symbol with a smiling jelly face embossed, glossy game icon"),
- ("symbol_seven", "a glossy candy-styled lucky number seven symbol, red and gold, jelly texture, game slot icon"),
- ],
- "fruit": [
- ("symbol_cherry", "a glossy cherry pair slot symbol with cute eyes, premium 3D mobile game icon"),
- ("symbol_lemon", "a bright lemon slice slot symbol, glossy and juicy, cute arcade style"),
- ("symbol_grape", "a purple grape cluster slot symbol, shiny 3D fruit icon"),
- ("symbol_watermelon", "a watermelon wedge slot symbol with juicy shine and clean edges"),
- ("symbol_orange", "an orange citrus slot symbol, glossy mobile game icon"),
- ("symbol_bell", "a golden bell slot symbol with fruit stickers and soft glow"),
- ("symbol_coin", "a shiny golden coin fruit slot symbol"),
- ("symbol_seven", "a red lucky seven fruit slot symbol with gold trim"),
- ],
- "pirate_jelly": [
- ("jelly_pirate_red", "a red translucent jelly pirate mascot with a black skull pirate hat, winking, glossy gummy texture, cute mobile slot symbol"),
- ("jelly_pirate_pink", "a pink jelly candy pirate mascot holding a tiny gold coin, cheerful face, glossy 3D gummy texture"),
- ("symbol_treasure_chest", "a purple and gold pirate treasure chest overflowing with bright gold coins, glossy 3D mobile slot icon"),
- ("symbol_coin_multiplier", "a glowing golden coin multiplier badge reading 20x style, no small text, premium hold and win coin symbol"),
- ("symbol_pirate_hat", "a black pirate hat with skull emblem, candy glossy trim, cute mobile game slot icon"),
- ("symbol_parrot", "a colorful cute pirate parrot, glossy 3D mobile slot symbol, clean edges"),
- ("symbol_compass", "a golden pirate compass with blue glass, polished 3D icon"),
- ("symbol_map_scatter", "a rolled treasure map scatter symbol with red X, glossy parchment and gold trim, no tiny text"),
- ("symbol_anchor", "a candy-colored pirate anchor with gold rope, glossy 3D icon"),
- ("symbol_ship", "a cute miniature pirate ship on blue ocean waves, polished mobile slot icon"),
- ],
- }
- UI_ART = [
- ("bg_main", False, "1024x1536", "vertical mobile slot game background, clear top area for logo, central reel area, bright themed world, no characters, no text"),
- ("logo", True, "1024x1024", "glossy mobile slot game logo lettering, playful premium game title, thick outline, sparkles, transparent background"),
- ("reel_frame", True, "1024x1024", "rounded rectangle slot machine reel frame, glowing border, hollow transparent center, clean mobile game UI element"),
- ("btn_spin", True, "1024x1024", "large round glossy spin button with circular arrows icon, premium 3D mobile game button, transparent background"),
- ("btn_round", True, "1024x1024", "small round glossy secondary mobile game UI button, blank center, transparent background"),
- ("hud_pill", True, "1024x1024", "horizontal rounded pill shaped mobile game HUD panel, glossy dark translucent material, blank, transparent background"),
- ("win_popup", True, "1024x1024", "big win popup frame, glossy gold and candy highlights, blank center, transparent background"),
- ("free_spin_badge", True, "1024x1024", "free spins bonus badge, glossy premium mobile slot UI, blank center, transparent background"),
- ]
- def slugify(value):
- value = (value or "slot-game").strip().lower()
- value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
- return value or "slot-game"
- def clamp_int(value, default, lo, hi):
- try:
- value = int(value)
- except (TypeError, ValueError):
- value = default
- return max(lo, min(hi, value))
- def build_symbol_rules(symbols):
- count = max(1, len(symbols))
- rules = []
- for index, (sid, _prompt) in enumerate(symbols):
- if sid == "wild" or sid.startswith("wild"):
- role = "wild"
- tier = "special"
- weight = 3
- elif sid == "scatter" or "scatter" in sid:
- role = "scatter"
- tier = "special"
- weight = 2
- elif sid in ("coin_cash", "collect") or "coin" in sid:
- role = "bonus"
- tier = "bonus"
- weight = 3
- else:
- role = "regular"
- pct = index / count
- if pct < 0.2:
- tier = "premium"
- elif pct < 0.55:
- tier = "mid"
- else:
- tier = "low"
- weight = max(6, 22 - index)
- rules.append({"id": sid, "role": role, "tier": tier, "weight": weight})
- return rules
- def build_paytable(symbol_rules, volatility):
- vol = VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])
- mult = vol["payMultiplier"]
- tier_base = {"premium": 10, "mid": 6, "low": 3, "bonus": 2, "special": 0}
- paytable = {}
- for item in symbol_rules:
- base = tier_base.get(item["tier"], 3)
- if item["role"] == "wild":
- paytable[item["id"]] = {"3": 0, "4": 0, "5": 0, "6": 0, "note": "substitutes_regular_symbols"}
- elif item["role"] == "scatter":
- paytable[item["id"]] = {"3": 4, "4": 12, "5": 40, "6": 100, "note": "pays_anywhere_and_triggers_free_spins"}
- else:
- paytable[item["id"]] = {
- "3": round(base * mult, 2),
- "4": round(base * 2.4 * mult, 2),
- "5": round(base * 6.0 * mult, 2),
- "6": round(base * 14.0 * mult, 2),
- }
- return paytable
- def clamp_float(value, default, lo, hi):
- try:
- value = float(value)
- except (TypeError, ValueError):
- value = default
- if value > 1:
- value = value / 100.0
- return max(lo, min(hi, value))
- def build_slot_config(data):
- theme = data.get("theme", "jelly")
- reel_mode = data.get("reelMode", "ways")
- volatility = data.get("volatility", "medium")
- default_features = ["cascades", "free_spins", "wilds"]
- if theme == "pirate_jelly":
- default_features = ["cascades", "free_spins", "wilds", "hold_win", "multipliers"]
- feature_names = set(data.get("features") or default_features)
- columns = 6 if reel_mode == "megaways" else 5
- rows = 5 if reel_mode == "cluster" else 3
- return {
- "schemaVersion": "slot_game_config.v1",
- "creative": data.get("creative", {}),
- "gameDesign": data.get("gameDesign", {}),
- "game": {
- "id": slugify(data.get("gameId") or data.get("title") or "jelly-candy-slot"),
- "title": data.get("title") or "Jelly Candy Slot",
- "mode": "demo_only",
- "engine": "cocos_creator_3_8",
- "orientation": "portrait",
- "targetViewport": {"width": 798, "height": 1724},
- },
- "theme": {
- "key": theme,
- "world": THEME_LABELS.get(theme, theme),
- "visualStyle": data.get("style") or DEFAULT_STYLE_BY_THEME.get(theme, DEFAULT_STYLE_BY_THEME["jelly"]),
- },
- "reels": {
- "mode": reel_mode,
- "columns": columns,
- "rows": rows,
- },
- "layout": {
- "viewport": {"width": 798, "height": 1724, "orientation": "portrait"},
- "logo": {"cy": 0.41, "maxW": 0.82, "maxH": 0.15},
- "reel": {
- "cy": 0.02,
- "h": 0.60,
- "aspect": 0.652,
- "holeW": 0.86,
- "holeH": 0.92,
- "cols": columns,
- "rows": rows,
- },
- "hud": {"cy": -0.33, "pillH": 0.40},
- "controls": {"cy": -0.405, "spinR": 0.13, "smallR": 0.075, "xMinus": 0.24, "xTurbo": 0.40},
- "symbols": {
- "fitMode": "contain",
- "targetCellFill": 0.92,
- "defaultScalePerCell": 0.00093,
- "defaultOriginYOffsetPerCell": 0.5,
- },
- },
- "mathProfile": {
- "volatility": volatility,
- "hitFrequencyFeel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["hitFrequencyFeel"],
- "rtpTarget": clamp_float(data.get("targetRtp"), 0.96, 0.5, 0.995),
- "rtpTargetLabel": "math_model_candidate" if data.get("enableMathModel", True) else "configurable_demo_no_math_model",
- "enableMathModel": bool(data.get("enableMathModel", True)),
- "bonusChanceLabel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["bonusChanceLabel"],
- },
- "economy": {
- "startingBalance": clamp_int(data.get("startingBalance"), 5000, 100, 1000000),
- "defaultBet": clamp_int(data.get("defaultBet"), 50, 1, 100000),
- "minBet": 10,
- "maxBet": 1000,
- "betSteps": [10, 20, 50, 100, 200, 500, 1000],
- },
- "winRules": {
- "evaluation": "cluster_count" if reel_mode == "cluster" else ("left_to_right_paylines" if reel_mode == "paylines" else "adjacent_ways_demo"),
- "minMatch": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["minMatch"],
- "paylines": 20 if reel_mode == "paylines" else 0,
- "ways": columns ** rows if reel_mode in ("ways", "megaways") else 0,
- },
- "features": {
- "wilds": {"enabled": "wilds" in feature_names, "variant": data.get("wildVariant", "expanding"), "substitutes": "regular"},
- "scatterFreeSpins": {
- "enabled": "free_spins" in feature_names,
- "triggerCount": 3,
- "awardSpins": clamp_int(data.get("freeSpinCount"), 8, 3, 30),
- "retriggers": True,
- },
- "cascades": {
- "enabled": "cascades" in feature_names,
- "maxCascades": clamp_int(data.get("maxCascades"), 6, 1, 20),
- "multiplierStep": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["cascadeMultiplierStep"],
- "maxMultiplier": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["maxCascadeMultiplier"],
- },
- "holdAndWin": {"enabled": "hold_win" in feature_names, "triggerCount": 6, "respins": 3, "jackpots": ["mini", "minor", "major"]},
- "multipliers": {"enabled": "multipliers" in feature_names},
- },
- "boss": {
- "enabled": bool(data.get("enableBoss", True)),
- "id": data.get("bossId") or "boss_demon_lord",
- "title": data.get("bossTitle") or "大魔王关主",
- "presence": data.get("bossPresence") or "full",
- "idleLoop": ["idle", "watch", "charge"],
- "winReaction": "boss_cracks_open_and_explodes_when_player_wins",
- "loseReaction": "boss_raises_greatsword_and_taunts_after_player_loses",
- "playerWinSequence": ["coin_throw", "hurt", "explode"],
- "playerLoseSequence": ["taunt", "stomp", "attack"],
- },
- "feedback": FEEDBACK_RULES.get(data.get("feedbackIntensity", "standard"), FEEDBACK_RULES["standard"]),
- "assetGeneration": {
- "characterCount": int(data.get("characterCount") or 10),
- "uiCompleteness": data.get("uiCompleteness", "full"),
- "feedbackIntensity": data.get("feedbackIntensity", "standard"),
- },
- }
- def build_manifest(slot_config):
- slot_config = copy.deepcopy(slot_config)
- theme = slot_config["theme"]["key"]
- style = slot_config["theme"]["visualStyle"]
- count = slot_config["assetGeneration"]["characterCount"]
- base_symbols = copy.deepcopy(BASE_SYMBOLS.get(theme, BASE_SYMBOLS["jelly"]))
- special_symbols = []
- if slot_config["features"]["wilds"]["enabled"]:
- special_symbols.append(("wild", "a glossy rainbow WILD slot symbol icon, premium mobile game asset, no small text"))
- if slot_config["features"]["scatterFreeSpins"]["enabled"]:
- special_symbols.append(("scatter", "a glowing scatter bonus candy star symbol, premium mobile slot game icon, no small text"))
- if slot_config["features"]["holdAndWin"]["enabled"]:
- special_symbols.extend([
- ("coin_cash", "a shiny golden cash coin slot symbol with soft glow, premium mobile game icon"),
- ("collect", "a glossy collect symbol with magnet-like gold energy, premium mobile slot game icon"),
- ])
- regular_count = max(1, count - len(special_symbols))
- symbols = base_symbols[:regular_count] + special_symbols
- symbol_rules = build_symbol_rules(symbols)
- slot_config["symbols"] = symbol_rules
- slot_config["paytable"] = build_paytable(symbol_rules, slot_config["mathProfile"]["volatility"])
- if slot_config.get("mathProfile", {}).get("enableMathModel", True):
- slot_config["mathModel"] = slot_math.build_math_model(slot_config)
- else:
- slot_config["mathModel"] = {
- "status": "disabled_by_user",
- "notes": ["Math model generation was disabled for this configurable demo."],
- }
- vfx = [
- {"id": "win_burst", "type": "particle", "template": "burst", "color": [255, 180, 80]},
- ]
- if slot_config["features"]["cascades"]["enabled"]:
- vfx.append({"id": "confetti_pop", "type": "particle", "template": "confetti", "color": [255, 120, 200]})
- if slot_config["features"]["scatterFreeSpins"]["enabled"] or slot_config["features"]["holdAndWin"]["enabled"]:
- vfx.append({"id": "bigwin_glow", "type": "particle", "template": "glow", "color": [255, 240, 160]})
- if slot_config["features"]["holdAndWin"]["enabled"] or slot_config["assetGeneration"]["feedbackIntensity"] == "loud":
- vfx.append({"id": "coin_rain", "type": "particle", "template": "rain", "color": [255, 215, 0]})
- if slot_config.get("boss", {}).get("enabled", True):
- vfx.append({"id": "boss_explosion", "type": "particle", "template": "burst", "color": [190, 80, 255]})
- ui = [
- {"id": "spin_btn_press", "type": "tween", "preset": "scale_bounce"},
- {"id": "reward_popup_in", "type": "tween", "preset": "elastic_in"},
- {"id": "panel_slide_in", "type": "tween", "preset": "fade_slide_in", "params": {"dy": 60}},
- {"id": "balance_roll", "type": "tween", "preset": "number_roll", "params": {"from": 0, "to": 8888, "dur": 0.9}},
- {"id": "win_icon_pulse", "type": "tween", "preset": "pulse"},
- ]
- ui_art_ids = {"basic": 6, "full": len(UI_ART)}
- ui_art_count = ui_art_ids.get(slot_config["assetGeneration"]["uiCompleteness"], len(UI_ART))
- characters = [
- {"id": sid, "type": "spine", "animations": ["idle", "win"], "prompt": prompt}
- for sid, prompt in symbols
- ]
- if slot_config.get("boss", {}).get("enabled", True):
- boss_id = slot_config["boss"]["id"]
- characters.append({
- "id": boss_id,
- "type": "spine_parts",
- "role": "boss",
- "animations": ["idle", "watch", "charge", "coin_throw", "taunt", "stomp", "attack", "hurt", "explode"],
- "prompt": (
- "a cute stylized mobile game demon lord boss character, full body, oversized dark armor, "
- "horned crown, glowing eyes, holding a huge greatsword, triumphant villain pose, one boot "
- "planted forward on a simple defeated hero silhouette, no gore, no blood, clean mobile slot "
- "game boss asset, transparent background"
- ),
- "parts": [
- {"id": "torso", "parent": "root", "x": 0, "y": 180, "prompt": "torso armor chest of a cute demon lord boss, cracked dark armor, front view"},
- {"id": "head", "parent": "torso", "x": 0, "y": 260, "prompt": "horned demon lord head with glowing eyes and crown, cute villain mobile game style"},
- {"id": "left_arm", "parent": "torso", "x": -170, "y": 160, "prompt": "left armored arm of demon lord boss with clawed gauntlet"},
- {"id": "right_arm", "parent": "torso", "x": 170, "y": 170, "prompt": "right armored arm of demon lord boss gripping a sword handle"},
- {"id": "greatsword", "parent": "right_arm", "x": 160, "y": 170, "prompt": "oversized dark fantasy greatsword, mobile game boss weapon, clean icon asset"},
- {"id": "left_leg", "parent": "torso", "x": -85, "y": -80, "prompt": "left armored boot leg of demon lord boss, heavy boot"},
- {"id": "right_leg", "parent": "torso", "x": 90, "y": -80, "prompt": "right armored boot leg of demon lord boss stepping forward"},
- {"id": "cape", "parent": "torso", "x": -40, "y": 120, "prompt": "torn purple villain cape piece for demon lord boss"},
- {"id": "horn_left", "parent": "head", "x": -70, "y": 70, "prompt": "left curved horn of demon lord boss"},
- {"id": "horn_right", "parent": "head", "x": 70, "y": 70, "prompt": "right curved horn of demon lord boss"},
- {"id": "crack_core", "parent": "torso", "x": 0, "y": 160, "prompt": "glowing purple gold magical crack core explosion light for boss armor"},
- {"id": "coin_bag", "parent": "left_arm", "x": -75, "y": -45, "prompt": "small purple villain coin bag overflowing with shiny gold coins, mobile game boss prop"},
- {"id": "coin_splash", "parent": "torso", "x": 0, "y": 220, "prompt": "arc of shiny gold coins flying outward, transparent background, clean mobile game VFX prop"},
- {"id": "defeated_hero_shadow", "parent": "root", "x": 95, "y": -185, "prompt": "tiny simple defeated hero silhouette shadow under a demon boss boot, comedic mobile game style, no gore, no blood"},
- ],
- })
- return {
- "game": slot_config["game"]["id"],
- "style": style,
- "slot_config": slot_config,
- "characters": characters,
- "ui_art": [
- {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
- for aid, transparent, size, prompt in UI_ART[:ui_art_count]
- ],
- "vfx": vfx,
- "ui": ui,
- }
- def build_workflow(data):
- slot_config = build_slot_config(data or {})
- manifest = build_manifest(slot_config)
- return {"slot_config": manifest["slot_config"], "manifest": manifest}
|