feat: add core data model (Card, Deck, RuleConfig, Constants)
This commit is contained in:
70
src/core/card.gd
Normal file
70
src/core/card.gd
Normal file
@@ -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]]
|
||||||
77
src/core/constants.gd
Normal file
77
src/core/constants.gd
Normal file
@@ -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}
|
||||||
58
src/core/deck.gd
Normal file
58
src/core/deck.gd
Normal file
@@ -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()
|
||||||
40
src/core/rule_config.gd
Normal file
40
src/core/rule_config.gd
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user