File size: 5,065 Bytes
1c66cf8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""Tests for features/plot_value.py."""
from __future__ import annotations

from werewolftown.features.plot_value import (
    find_best_buy_candidate,
    plot_buyer_gain,
    plot_opportunity_cost,
    plot_value,
)
from werewolftown.state.game_state import PlayerView


class TestPlotValue:
    def test_empty_plot_best_shop(self, make_state):
        state = make_state(
            plots=frozenset({4, 5, 6}),
            built_on={4: "Happy", 5: "Happy"},
            shops=("Happy",),
        )
        # Plot 6 adjacent to 5 (Happy) → gain = rev(3,4) - rev(2,4) = 50000-30000
        val = plot_value(state, 6, viewer="me")
        assert val == 20000

    def test_built_plot_fixed_type(self, make_state):
        state = make_state(
            plots=frozenset({4, 5, 6}),
            built_on={4: "Happy", 5: "Happy", 6: "猫天天"},
            shops=(),
        )
        # Plot 6 is built with 猫天天 → gain from acquiring = 0 (already built, fixed type)
        # Actually, gain is the marginal revenue from having this plot
        # For a built plot, it's the cluster gain for that type
        val = plot_value(state, 6, viewer="me")
        # 6 is isolated 猫天天 (no adjacent 猫天天) → gain = 0
        assert val == 0

    def test_isolated_empty_plot(self, make_state):
        state = make_state(
            plots=frozenset({4, 5, 7}),
            built_on={4: "Happy"},
            shops=("Happy",),
        )
        # Plot 7 not adjacent to any Happy → can still build 1-cluster = 10000
        val = plot_value(state, 7, viewer="me")
        assert val == 10000


class TestPlotOpportunityCost:
    def test_built_plot_loss(self, make_state, cfg):
        state = make_state(
            round=1,
            plots=frozenset({4, 5}),
            built_on={4: "Happy", 5: "Happy"},
            shops=(),
        )
        # Selling plot 4: lose 2-cluster → 1-cluster
        # rev(2,4)=30000, rev(1,4)=10000 → loss=20000/round * 4 rounds = 80000
        # + extra_penalty 5000*4 = 20000 → total = 100000
        cost = plot_opportunity_cost(state, 4, cfg)
        assert cost == 100000

    def test_empty_plot_adjacent(self, make_state, cfg):
        state = make_state(
            round=1,
            plots=frozenset({4, 5, 6}),
            built_on={4: "Happy", 5: "Happy"},
            shops=("Happy",),
        )
        # Empty plot 6, adjacent to my plots → floor=10000 * 4 = 40000
        cost = plot_opportunity_cost(state, 6, cfg)
        assert cost == 40000

    def test_empty_plot_isolated(self, make_state, cfg):
        state = make_state(
            round=1,
            plots=frozenset({4, 5, 7}),
            built_on={4: "Happy", 5: "Happy"},
            shops=("Happy",),
        )
        # Empty plot 7, not adjacent → floor=5000 * 4 = 20000
        cost = plot_opportunity_cost(state, 7, cfg)
        assert cost == 20000


class TestPlotBuyerGain:
    def test_buyer_gain_with_penalty(self, make_state, cfg):
        state = make_state(
            round=1,
            player_id="子涵",
            plots=frozenset({4, 5}),
            built_on={4: "Happy", 5: "Happy"},
            shops=("Happy",),
            other_players={"浩宇": PlayerView(
                name="浩宇", cash=50000,
                plots=frozenset({6, 10}),
                shops=("Happy",),
            )},
        )
        # 浩宇 has Happy card, plot 6 → can build 1-cluster = 10000/round
        # penalty ratio R1 = 0.4, remaining = 4 → 10000 * 4 * 0.4 = 16000
        gain = plot_buyer_gain(state, 6, "浩宇", cfg)
        assert gain == 16000

    def test_zero_for_no_gain(self, make_state, cfg):
        state = make_state(round=1)
        gain = plot_buyer_gain(state, 4, "浩宇", cfg)
        assert gain == 0


class TestFindBestBuyCandidate:
    def test_finds_adjacent_opp_plot(self, make_state, cfg):
        state = make_state(
            round=1,
            player_id="子涵",
            plots=frozenset({4, 5}),
            built_on={4: "Happy", 5: "Happy"},
            shops=("Happy",),
            other_players={"浩宇": PlayerView(
                name="浩宇", cash=50000,
                plots=frozenset({6}),  # 6 adj 5
                shops=(),
            )},
        )
        cand = find_best_buy_candidate(state, cfg)
        assert cand is not None
        assert cand.plot_id == 6
        assert cand.owner == "浩宇"
        assert cand.is_adjacent is True
        assert cand.gain > 0

    def test_none_when_no_adjacent(self, make_state, cfg):
        state = make_state(
            plots=frozenset({4, 5}),
            other_players={"浩宇": PlayerView(
                name="浩宇", cash=50000,
                plots=frozenset({10}),  # 10 adj 9, not adj to 4/5
                shops=(),
            )},
        )
        assert find_best_buy_candidate(state, cfg) is None

    def test_none_when_no_opponents(self, make_state, cfg):
        state = make_state(plots=frozenset({4, 5}))
        assert find_best_buy_candidate(state, cfg) is None