From 016098f213cd83efed7e04c9e9b10629aa4a866e Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 29 May 2026 09:06:49 +0800 Subject: [PATCH] feat: add core data model (Card, Deck, RuleConfig, Constants) --- src/core/card.gd | 70 +++++++++++++++++++++++++++++++++++++ src/core/constants.gd | 77 +++++++++++++++++++++++++++++++++++++++++ src/core/deck.gd | 58 +++++++++++++++++++++++++++++++ src/core/rule_config.gd | 40 +++++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 src/core/card.gd create mode 100644 src/core/constants.gd create mode 100644 src/core/deck.gd create mode 100644 src/core/rule_config.gd diff --git a/src/core/card.gd b/src/core/card.gd new file mode 100644 index 0000000..f0b5d0a --- /dev/null +++ b/src/core/card.gd @@ -0,0 +1,70 @@ +# src/core/card.gd +# Pure data — no Godot dependencies + +class_name Card +extends RefCounted + +const MIN_ID := 0 +const MAX_ID := 107 + +var card_id: int +var original_id: int +var _suit: int +var _rank: int + +static func create(original_id: int, suit: int, rank: int) -> Card: + var c := Card.new() + c.original_id = original_id + c._suit = suit + c._rank = rank + c.card_id = original_id + return c + +static func make_full_deck_ids() -> Array[int]: + var ids: Array[int] = [] + for i in range(108): + ids.append(i) + return ids + +static func card_id_from_deck(original_id: int, deck_index: int) -> int: + return original_id + deck_index * 54 + +func suit() -> int: + return _suit + +func rank() -> int: + return _rank + +func card_value() -> int: + return _suit * 20 + _rank + +func is_joker() -> bool: + return _rank == 15 or _rank == 16 + +func is_heart() -> bool: + return _suit == 1 + +func matches(other: Card) -> bool: + return card_value() == other.card_value() + +func equals(other: Card) -> bool: + return card_id == other.card_id + +func compare_to(other: Card) -> int: + var r := _rank - other._rank + if r != 0: + return r + return _suit - other._suit + +func to_packed() -> int: + return (_suit << 8) | (_rank & 0xFF) + +static func from_packed(packed: int) -> Card: + var suit := (packed >> 8) & 0xFF + var rank := packed & 0xFF + return Card.create(0, suit, rank) + +func _to_string() -> String: + var suits := ["S", "H", "C", "D", "SJ", "BJ"] + var ranks := ["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "SJ", "BJ"] + return "%s%s" % [suits[_suit], ranks[_rank]] diff --git a/src/core/constants.gd b/src/core/constants.gd new file mode 100644 index 0000000..e1efc92 --- /dev/null +++ b/src/core/constants.gd @@ -0,0 +1,77 @@ +# src/core/constants.gd +# Zero Godot dependency — no class_name, use static functions to avoid global state. + +const RESULT_OK := 0 +const ERR_INVALID_CARDS := 1 +const ERR_NOT_YOUR_TURN := 2 +const ERR_CANNOT_PASS := 3 +const ERR_CARD_NOT_FOUND := 4 +const ERR_HAND_EMPTY := 5 +const ERR_INVALID_TRIBUTE := 6 +const ERR_GAME_OVER := 7 +const ERR_STATE_CONFLICT := 8 + +const SUIT_SPADE := 0 +const SUIT_HEART := 1 +const SUIT_CLUB := 2 +const SUIT_DIAMOND := 3 +const SUIT_JOKER_SMALL := 4 +const SUIT_JOKER_BIG := 5 + +const RANK_2 := 2 +const RANK_3 := 3 +const RANK_4 := 4 +const RANK_5 := 5 +const RANK_6 := 6 +const RANK_7 := 7 +const RANK_8 := 8 +const RANK_9 := 9 +const RANK_10 := 10 +const RANK_J := 11 +const RANK_Q := 12 +const RANK_K := 13 +const RANK_A := 14 +const RANK_SMALL_JOKER := 15 +const RANK_BIG_JOKER := 16 + +const TYPE_SINGLE := 0 +const TYPE_PAIR := 1 +const TYPE_TRIPLE := 2 +const TYPE_TRIPLE_PLUS_TWO := 3 +const TYPE_STRAIGHT := 4 +const TYPE_CONSECUTIVE_PAIRS := 5 +const TYPE_STEEL_PLATE := 6 +const TYPE_STRAIGHT_FLUSH := 7 +const TYPE_BOMB := 8 +const TYPE_ROCKET := 9 + +const TURN_PLAY := 0 +const TURN_PASS := 1 +const TURN_TRIBUTE_GIVE := 2 +const TURN_TRIBUTE_RETURN := 3 + +const PHASE_INIT := 0 +const PHASE_DEAL := 1 +const PHASE_TRIBUTE := 2 +const PHASE_PLAY := 3 +const PHASE_LEVEL_UP := 4 +const PHASE_GAME_OVER := 5 + +const WILD_SOURCE_GRADE := 0 +const WILD_SOURCE_SYSTEM := 1 + +const MAX_AI_DECISION_MS := 3000 +const BEAM_WIDTH := 50 +const MAX_ENUM_NODES := 10000 +const TOTAL_CARDS := 108 +const CARDS_PER_PLAYER := 27 +const PLAYER_COUNT := 4 + +static func make_result(ok: bool, error_code: int, data = null) -> Dictionary: + return {"ok": ok, "error_code": error_code, "data": data} + +static func success(data = null) -> Dictionary: + return {"ok": true, "error_code": RESULT_OK, "data": data} + +static func err(code: int) -> Dictionary: + return {"ok": false, "error_code": code, "data": null} diff --git a/src/core/deck.gd b/src/core/deck.gd new file mode 100644 index 0000000..462ef13 --- /dev/null +++ b/src/core/deck.gd @@ -0,0 +1,58 @@ +# src/core/deck.gd +class_name Deck +extends RefCounted + +var _cards: Array[Card] = [] + +static func _suit_for(original_id: int) -> int: + if original_id == 52: + return 4 + if original_id == 53: + return 5 + return original_id % 4 + +static func _rank_for(original_id: int) -> int: + if original_id == 52: + return 15 + if original_id == 53: + return 16 + return 2 + (original_id / 4) + +static func create(seed: int = -1) -> Deck: + var d := Deck.new() + d._cards = [] + for deck_idx in range(2): + for orig_id in range(54): + var global_id := Card.card_id_from_deck(orig_id, deck_idx) + var suit := _suit_for(orig_id) + var rank := _rank_for(orig_id) + var c := Card.create(orig_id, suit, rank) + c.card_id = global_id + d._cards.append(c) + if seed >= 0: + d._shuffle_with_seed(seed) + else: + d._shuffle_random() + return d + +func _shuffle_with_seed(seed: int) -> void: + var rng := RandomNumberGenerator.new() + rng.seed = seed + for i in range(_cards.size() - 1, 0, -1): + var j := rng.randi_range(0, i) + var tmp := _cards[i] + _cards[i] = _cards[j] + _cards[j] = tmp + +func _shuffle_random() -> void: + _shuffle_with_seed(Time.get_unix_time_from_system() as int) + +func deal(count: int) -> Array[Card]: + var result: Array[Card] = [] + var actual := mini(count, _cards.size()) + for _i in range(actual): + result.append(_cards.pop_back()) + return result + +func remaining() -> int: + return _cards.size() diff --git a/src/core/rule_config.gd b/src/core/rule_config.gd new file mode 100644 index 0000000..8a82324 --- /dev/null +++ b/src/core/rule_config.gd @@ -0,0 +1,40 @@ +# src/core/rule_config.gd +class_name RuleConfig +extends RefCounted + +const BOMB_BY_RANK := 0 +const BOMB_BY_COUNT := 1 + +const TEAM_FIXED_1_3 := 0 +const TEAM_RANDOM := 1 + +var same_suit_straight_beats_bomb: bool = true +var double_down_levels: int = 3 +var can_tribute_wild: bool = false +var tribute_return_max_rank: int = 10 +var straight_extends_to_ace: bool = true +var bomb_compare_priority: int = BOMB_BY_RANK +var wild_count: int = 2 +var team_formation: int = TEAM_FIXED_1_3 + +static func standard() -> RuleConfig: + var c := RuleConfig.new() + return c + +static func huaian() -> RuleConfig: + var c := RuleConfig.new() + c.same_suit_straight_beats_bomb = false + c.straight_extends_to_ace = false + return c + +func to_hash_data() -> Dictionary: + return { + "same_suit_bomb": same_suit_straight_beats_bomb, + "double_down": double_down_levels, + "can_tribute_wild": can_tribute_wild, + "tribute_return_max": tribute_return_max_rank, + "straight_ace": straight_extends_to_ace, + "bomb_compare": bomb_compare_priority, + "wild_count": wild_count, + "team": team_formation + }