diff --git a/tests/test_cards.gd b/tests/test_cards.gd new file mode 100644 index 0000000..8830dbe --- /dev/null +++ b/tests/test_cards.gd @@ -0,0 +1,42 @@ +extends GutTest + +func test_card_create(): + var card = Card.create(0, 3, 14) + assert_eq(card.card_id, 0) + assert_eq(card.original_id, 0) + assert_eq(card.suit(), 3) + assert_eq(card.rank(), 14) + +func test_card_equality(): + var a := Card.create(0, 2, 10) + var b := Card.create(1, 2, 10) + assert_true(a.matches(b)) + assert_false(a.equals(b)) + +func test_card_id_range(): + assert_eq(Card.MIN_ID, 0) + assert_eq(Card.MAX_ID, 107) + +func test_is_joker(): + var small := Card.create(52, 4, 15) + var big := Card.create(53, 5, 16) + assert_true(small.is_joker()) + assert_true(big.is_joker()) + +func test_compare_to(): + var low := Card.create(0, 0, 5) + var high := Card.create(1, 1, 14) + assert_lt(low.compare_to(high), 0) + +func test_card_id_from_deck(): + assert_eq(Card.card_id_from_deck(0, 0), 0) + assert_eq(Card.card_id_from_deck(0, 1), 54) + assert_eq(Card.card_id_from_deck(53, 0), 53) + assert_eq(Card.card_id_from_deck(53, 1), 107) + +func test_packed(): + var c := Card.create(10, 1, 13) + var packed := c.to_packed() + var unpacked := Card.from_packed(packed) + assert_eq(unpacked.suit(), 1) + assert_eq(unpacked.rank(), 13) diff --git a/tests/test_deck.gd b/tests/test_deck.gd new file mode 100644 index 0000000..0dda233 --- /dev/null +++ b/tests/test_deck.gd @@ -0,0 +1,35 @@ +extends GutTest + +func test_deck_has_108_cards(): + var deck := Deck.create() + assert_eq(deck.remaining(), 108) + +func test_deal_reduces_count(): + var deck := Deck.create() + var cards := deck.deal(27) + assert_eq(cards.size(), 27) + assert_eq(deck.remaining(), 81) + +func test_shuffle_deterministic(): + var seed := 12345 + var deck1 := Deck.create(seed) + var deck2 := Deck.create(seed) + var hand1 := deck1.deal(108) + var hand2 := deck2.deal(108) + for i in range(108): + assert_eq(hand1[i].card_id, hand2[i].card_id) + +func test_deal_empty_deck(): + var deck := Deck.create() + deck.deal(108) + var extra := deck.deal(1) + assert_eq(extra.size(), 0) + +func test_deck_card_ids_unique(): + var deck := Deck.create() + var all_cards := deck.deal(108) + var ids := {} + for c in all_cards: + assert_false(ids.has(c.card_id)) + ids[c.card_id] = true + assert_eq(ids.size(), 108) diff --git a/tests/test_game_state.gd b/tests/test_game_state.gd new file mode 100644 index 0000000..23def3c --- /dev/null +++ b/tests/test_game_state.gd @@ -0,0 +1,74 @@ +extends GutTest + +var _config: RuleConfig +const _C = preload("res://src/core/constants.gd") + +func before_each(): + _config = RuleConfig.standard() + +func _card(orig_id: int, deck_idx: int = 0) -> Card: + var suit: int + var rank: int + if orig_id == 52: + suit = 4; rank = 15 + elif orig_id == 53: + suit = 5; rank = 16 + else: + suit = orig_id % 4 + rank = 2 + (orig_id / 4) + var c := Card.create(orig_id, suit, rank) + c.card_id = Card.card_id_from_deck(orig_id, deck_idx) + return c + +func _cards(ids: Array) -> Array[Card]: + var result: Array[Card] = [] + for id in ids: + result.append(_card(id as int)) + return result + +func test_generate_includes_pass(): + var hand := _cards([0]) + var moves := MoveGenerator.generate(hand, 5, _config) + var has_pass := false + for m in moves: + if m.type == -1 and m.cards.is_empty(): + has_pass = true + assert_true(has_pass) + +func test_generate_singles(): + var hand := _cards([0, 4, 8]) + var moves := MoveGenerator.generate(hand, 5, _config) + var singles := 0 + for m in moves: + if m.type == _C.TYPE_SINGLE: + singles += 1 + assert_gt(singles, 0) + +func test_generate_pair(): + var hand: Array[Card] = [_card(0, 0), _card(0, 1), _card(4, 0)] + var moves := MoveGenerator.generate(hand, 5, _config) + var has_pair := false + for m in moves: + if m.type == _C.TYPE_PAIR: + has_pair = true + assert_true(has_pair) + +func test_empty_hand_returns_pass_only(): + var hand: Array[Card] = [] + var moves := MoveGenerator.generate(hand, 5, _config) + assert_eq(moves.size(), 1) + assert_eq(moves[0].type, -1) + +func test_rule_config_standard(): + var rc := RuleConfig.standard() + assert_eq(rc.double_down_levels, 3) + assert_false(rc.can_tribute_wild) + +func test_rule_config_huaian(): + var rc := RuleConfig.huaian() + assert_false(rc.same_suit_straight_beats_bomb) + +func test_rule_config_hash(): + var rc := RuleConfig.standard() + var h := rc.to_hash_data() + assert_eq(h.double_down, 3) diff --git a/tests/test_hand_evaluator.gd b/tests/test_hand_evaluator.gd new file mode 100644 index 0000000..1a7e613 --- /dev/null +++ b/tests/test_hand_evaluator.gd @@ -0,0 +1,71 @@ +extends GutTest + +var _config: RuleConfig +const _C = preload("res://src/core/constants.gd") + +func before_each(): + _config = RuleConfig.standard() + +func _card(orig_id: int, deck_idx: int = 0) -> Card: + var suit: int + var rank: int + if orig_id == 52: + suit = 4; rank = 15 + elif orig_id == 53: + suit = 5; rank = 16 + else: + suit = orig_id % 4 + rank = 2 + (orig_id / 4) + var c := Card.create(orig_id, suit, rank) + c.card_id = Card.card_id_from_deck(orig_id, deck_idx) + return c + +func _cards(ids: Array) -> Array[Card]: + var result: Array[Card] = [] + for spec in ids: + if spec is int: + result.append(_card(spec as int)) + elif spec is Array: + result.append(_card(spec[0] as int, spec[1] as int)) + return result + +func test_single(): + var cards := _cards([0]) + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, _C.TYPE_SINGLE) + +func test_pair(): + # Two cards of same rank from different decks + var cards: Array[Card] = [_card(0, 0), _card(0, 1)] + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, _C.TYPE_PAIR) + +func test_straight(): + # 3,4,5,6,7: orig_ids 4,8,12,16,20 (all from deck 0) + var cards := _cards([4, 8, 12, 16, 20]) + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, _C.TYPE_STRAIGHT) + assert_eq(result.primary_rank, 7) + +func test_not_a_combo(): + var cards := _cards([0, 1, 2]) + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, HandEvaluator.INVALID) + +func test_bomb(): + # Four 2s: same orig_id from both decks + var cards: Array[Card] = [_card(0, 0), _card(0, 1), _card(1, 0), _card(1, 1)] + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, _C.TYPE_BOMB) + assert_eq(result.primary_rank, 2) + assert_true(result.is_pure_bomb) + +func test_rocket(): + var cards: Array[Card] = [_card(52, 0), _card(52, 1), _card(53, 0), _card(53, 1)] + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_eq(result.type, _C.TYPE_ROCKET) + +func test_evaluator_null_on_empty(): + var cards: Array[Card] = [] + var result := HandEvaluator.evaluate(cards, 5, _config) + assert_null(result) diff --git a/tests/test_rule_engine.gd b/tests/test_rule_engine.gd new file mode 100644 index 0000000..ef02111 --- /dev/null +++ b/tests/test_rule_engine.gd @@ -0,0 +1,77 @@ +extends GutTest + +var _config: RuleConfig +const _C = preload("res://src/core/constants.gd") + +func before_each(): + _config = RuleConfig.standard() + +func _card(orig_id: int, deck_idx: int = 0) -> Card: + var suit: int + var rank: int + if orig_id == 52: + suit = 4; rank = 15 + elif orig_id == 53: + suit = 5; rank = 16 + else: + suit = orig_id % 4 + rank = 2 + (orig_id / 4) + var c := Card.create(orig_id, suit, rank) + c.card_id = Card.card_id_from_deck(orig_id, deck_idx) + return c + +func _cards(ids: Array) -> Array[Card]: + var result: Array[Card] = [] + for spec in ids: + if spec is int: + result.append(_card(spec as int)) + elif spec is Array: + result.append(_card(spec[0] as int, spec[1] as int)) + return result + +func test_can_play_fresh_round(): + var hand: Array[Card] = [_card(0), _card(4)] + var table: Array = [] + var play := HandEvaluator.evaluate(_cards([0]), 5, _config) + var result := RuleEngine.can_play(hand, play, table, -1, 5, _config) + assert_true(result.ok) + +func test_pure_bomb_beats_mixed(): + var pure := HandEvaluator.EvaluatedPlay.new() + pure.type = _C.TYPE_BOMB + pure.primary_rank = 10 + pure.is_pure_bomb = true + var mixed := HandEvaluator.EvaluatedPlay.new() + mixed.type = _C.TYPE_BOMB + mixed.primary_rank = 10 + mixed.is_pure_bomb = false + assert_eq(RuleEngine.compare_bombs(pure, mixed, _config), 1) + assert_eq(RuleEngine.compare_bombs(mixed, pure, _config), -1) + +func test_rocket_beats_bomb(): + var rocket := HandEvaluator.EvaluatedPlay.new() + rocket.type = _C.TYPE_ROCKET + rocket.primary_rank = 999 + var bomb := HandEvaluator.EvaluatedPlay.new() + bomb.type = _C.TYPE_BOMB + bomb.primary_rank = 14 + assert_eq(RuleEngine.compare(rocket, bomb, _config), 1) + +func test_same_type_higher_beats(): + var low := HandEvaluator.EvaluatedPlay.new() + low.type = _C.TYPE_SINGLE; low.primary_rank = 3 + var high := HandEvaluator.EvaluatedPlay.new() + high.type = _C.TYPE_SINGLE; high.primary_rank = 14 + assert_eq(RuleEngine.compare(high, low, _config), 1) + +func test_cannot_play_card_not_in_hand(): + var hand: Array[Card] = [_card(4)] + var play := HandEvaluator.evaluate(_cards([0]), 5, _config) # card 0 not in hand + var result := RuleEngine.can_play(hand, play, [], -1, 5, _config) + assert_false(result.ok) + +func test_team_partner(): + var t := Actions.Team.create_team(0, 0, 2) + assert_eq(t.teammate_of(0), 2) + assert_eq(t.teammate_of(2), 0) + assert_true(t.contains(0))