고착생물을 집으로, 생물은 해양동물만
거품돌·해초·산호·켈프·관벌레는 물고기가 깃드는 자리지 그 자체가 일꾼이 아니다. 실제로 물고기는 바위 틈과 해초 사이를 보금자리로 삼는다. 전부 집으로 옮기고, 생물에는 움직이는 해양동물만 남겼다. - 집이 자연 서식지(rock/plant)와 지어 올린 것(vessel/wreck/pillar) 두 갈래가 되면서 정원에 폭이 생겼다. 거품돌 1, 해초·산호 2, 켈프·관벌레 4, 지어 올린 것은 표준 3. - 빈 자리를 메우려 게·해마·새우를 넣어 층마다 생물 둘을 맞췄다. 소라게·해마·열수구 새우. 형태도 셋 새로 그렸다. - 형태 계통이 그대로 경계가 된다. 생물 fish/glow/crab/seahorse/shrimp, 집 rock/plant/vessel/wreck/pillar. 겹치지 않는 것을 테스트가 지킨다. - 예전에 생물로 사 둔 돌·해초는 같은 id의 집으로 그만큼 지어 준다. 그냥 버리면 사 둔 것이 조용히 사라진다. 버그 둘 _fish_area/_orbit_center가 생물 자리를 grid_rect(화면 테두리)로 잘랐다. 가로로 이어 스크롤하던 시절에는 화면 = 바다여서 맞았지만, 층이 옆으로 늘어선 지금은 뒤쪽 층 생물이 전부 첫 층 오른쪽 끝으로 끌려와 제 층에서 사라졌다. _home_bounds()가 그 집이 속한 층의 테두리를 돌려준다. _make_fish가 fish와 glow만 궤도에 올려, 새로 넣은 게·해마·새우가 제자리에 굳어 있었다. 고착생물이 전부 집으로 갔으므로 가드를 걷어냈다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-56
@@ -58,9 +58,15 @@ func _rich_and_open() -> void:
|
||||
|
||||
|
||||
## 그 구역에 집을 한 채 세워 자리를 만든다. 세운 집의 인덱스를 돌려준다.
|
||||
## 정원이 표준(HOUSE_CAPACITY)인 집을 고른다 -- 거품돌처럼 정원이 다른 집을
|
||||
## 기준으로 삼으면 자리 계산 검사가 통째로 흔들린다.
|
||||
func _house_in(zone_index: int, id := "") -> int:
|
||||
if id.is_empty():
|
||||
id = Catalog.first_id_of_zone("house", zone_index)
|
||||
for d in Catalog.HOUSES:
|
||||
var same_zone := int(d.get("zone", 0)) == zone_index
|
||||
if same_zone and Catalog.capacity_of(String(d.id)) == Catalog.HOUSE_CAPACITY:
|
||||
id = String(d.id)
|
||||
break
|
||||
var spot := GameState.first_free_spot(id)
|
||||
return GameState.build_house(id, spot.x, spot.y)
|
||||
|
||||
@@ -82,8 +88,8 @@ func _test_starting_state() -> void:
|
||||
check("정원이 생물보다 넉넉하다", GameState.free_slots_in_zone(0) > 0,
|
||||
"%d / %d" % [GameState.fish_total, GameState.capacity_total()])
|
||||
check("시작 거품으로 한 마리를 더 들일 수 있다",
|
||||
GameState.bubbles >= GameState.cost_of("producer", "bubble_stone")
|
||||
and GameState.can_take_fish("bubble_stone"))
|
||||
GameState.bubbles >= GameState.cost_of("producer", "guppy")
|
||||
and GameState.can_take_fish("guppy"))
|
||||
|
||||
var s: Dictionary = Catalog.STARTER_HOUSES[0]
|
||||
check("집이 정해 둔 칸에 선다",
|
||||
@@ -100,11 +106,11 @@ func _test_auto_income() -> void:
|
||||
check("집만으로는 거품이 나지 않는다", is_equal_approx(GameState.bps, 0.0),
|
||||
str(GameState.bps))
|
||||
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
check("생물이 들어와야 거품이 난다", GameState.bps > 0.0, str(GameState.bps))
|
||||
|
||||
var before_bps := GameState.bps
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
check("생물이 늘면 초당 생산도 는다", GameState.bps > before_bps,
|
||||
"%f -> %f" % [before_bps, GameState.bps])
|
||||
|
||||
@@ -124,9 +130,9 @@ func _test_auto_income() -> void:
|
||||
func _test_cost_curves() -> void:
|
||||
print("가격 곡선")
|
||||
_rich_and_open()
|
||||
check("첫 거품돌은 15",
|
||||
is_equal_approx(GameState.cost_of("producer", "bubble_stone"), 15.0),
|
||||
str(GameState.cost_of("producer", "bubble_stone")))
|
||||
check("첫 구피는 15",
|
||||
is_equal_approx(GameState.cost_of("producer", "guppy"), 15.0),
|
||||
str(GameState.cost_of("producer", "guppy")))
|
||||
check("첫 항아리는 90",
|
||||
is_equal_approx(GameState.cost_of("house", "amphora"), 90.0),
|
||||
str(GameState.cost_of("house", "amphora")))
|
||||
@@ -136,8 +142,8 @@ func _test_cost_curves() -> void:
|
||||
check("집도 지을수록 비싸진다 (x1.15)",
|
||||
is_equal_approx(house_second, ceil(90.0 * 1.15)), str(house_second))
|
||||
|
||||
GameState.buy_fish("bubble_stone")
|
||||
var fish_second := GameState.cost_of("producer", "bubble_stone")
|
||||
GameState.buy_fish("guppy")
|
||||
var fish_second := GameState.cost_of("producer", "guppy")
|
||||
check("생물도 들일수록 비싸진다 (x1.15)",
|
||||
is_equal_approx(fish_second, ceil(15.0 * 1.15)), str(fish_second))
|
||||
check("서로 값을 간섭하지 않는다",
|
||||
@@ -207,10 +213,10 @@ func _test_zone_boundaries() -> void:
|
||||
func _test_capacity() -> void:
|
||||
print("정원")
|
||||
_rich_and_open()
|
||||
check("집이 없으면 한 마리도 못 들인다", not GameState.can_take_fish("bubble_stone"))
|
||||
check("사유를 알려준다", GameState.fish_blocker("bubble_stone").contains("빈 자리"),
|
||||
GameState.fish_blocker("bubble_stone"))
|
||||
check("거품이 넘쳐도 못 들인다", not GameState.buy_fish("bubble_stone"))
|
||||
check("집이 없으면 한 마리도 못 들인다", not GameState.can_take_fish("guppy"))
|
||||
check("사유를 알려준다", GameState.fish_blocker("guppy").contains("빈 자리"),
|
||||
GameState.fish_blocker("guppy"))
|
||||
check("거품이 넘쳐도 못 들인다", not GameState.buy_fish("guppy"))
|
||||
|
||||
_house_in(0)
|
||||
check("집을 지으면 정원이 생긴다",
|
||||
@@ -218,14 +224,14 @@ func _test_capacity() -> void:
|
||||
str(GameState.free_slots_in_zone(0)))
|
||||
|
||||
for i in Catalog.HOUSE_CAPACITY:
|
||||
check("%d번째 자리가 찬다" % (i + 1), GameState.buy_fish("bubble_stone"))
|
||||
check("%d번째 자리가 찬다" % (i + 1), GameState.buy_fish("guppy"))
|
||||
check("정원이 다 찼다", GameState.free_slots_in_zone(0) == 0,
|
||||
str(GameState.free_slots_in_zone(0)))
|
||||
check("정원이 차면 더 못 들인다", not GameState.buy_fish("bubble_stone"))
|
||||
check("종류를 바꿔도 마찬가지", not GameState.buy_fish("seaweed"))
|
||||
check("정원이 차면 더 못 들인다", not GameState.buy_fish("guppy"))
|
||||
check("종류를 바꿔도 마찬가지", not GameState.buy_fish("hermit_crab"))
|
||||
|
||||
_house_in(0)
|
||||
check("집을 더 지으면 다시 들일 수 있다", GameState.buy_fish("seaweed"))
|
||||
check("집을 더 지으면 다시 들일 수 있다", GameState.buy_fish("hermit_crab"))
|
||||
check("정원은 집 수 x %d" % Catalog.HOUSE_CAPACITY,
|
||||
GameState.capacity_in_zone(0) == 2 * Catalog.HOUSE_CAPACITY,
|
||||
str(GameState.capacity_in_zone(0)))
|
||||
@@ -253,7 +259,7 @@ func _test_move_house() -> void:
|
||||
check("실패했으면 제자리", GameState.house_at(4, 2) == index)
|
||||
check("제자리에 그대로 놓는 건 된다", GameState.move_house(index, 4, 2))
|
||||
|
||||
GameState.buy_fish("bubble_stone")
|
||||
GameState.buy_fish("guppy")
|
||||
check("살던 생물은 그대로 따라간다", GameState.move_house(index, 6, 4)
|
||||
and GameState.fish_total == 1)
|
||||
|
||||
@@ -279,7 +285,7 @@ func _test_remove_house() -> void:
|
||||
|
||||
# 살고 있는 생물이 갈 곳이 없으면 헐 수 없다.
|
||||
for i in Catalog.HOUSE_CAPACITY:
|
||||
GameState.buy_fish("bubble_stone")
|
||||
GameState.buy_fish("guppy")
|
||||
check("세입자가 꽉 찬 마지막 집은 못 헌다", not GameState.can_remove_house(0))
|
||||
check("헐려고 해도 아무것도 돌아오지 않는다",
|
||||
is_equal_approx(GameState.remove_house(0), 0.0))
|
||||
@@ -296,31 +302,31 @@ func _test_release_fish() -> void:
|
||||
print("생물 내보내기")
|
||||
_rich_and_open()
|
||||
_house_in(0)
|
||||
GameState.buy_fish("seaweed")
|
||||
var second_cost := GameState.cost_of("producer", "seaweed")
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
var second_cost := GameState.cost_of("producer", "hermit_crab")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
var before := GameState.bubbles
|
||||
var expected: float = floor(second_cost * Catalog.REFUND_RATE)
|
||||
|
||||
var refund := GameState.release_fish("seaweed")
|
||||
var refund := GameState.release_fish("hermit_crab")
|
||||
check("절반을 돌려받는다", is_equal_approx(refund, expected),
|
||||
"%f vs %f" % [refund, expected])
|
||||
check("잔액에 더해진다", is_equal_approx(GameState.bubbles, before + expected))
|
||||
check("마릿수가 줄었다", GameState.fish_count("seaweed") == 1)
|
||||
check("마릿수가 줄었다", GameState.fish_count("hermit_crab") == 1)
|
||||
check("자리가 다시 빈다",
|
||||
GameState.free_slots_in_zone(0) == Catalog.HOUSE_CAPACITY - 1,
|
||||
str(GameState.free_slots_in_zone(0)))
|
||||
|
||||
check("마지막 한 마리는 못 내보낸다", not GameState.can_release_fish("seaweed"))
|
||||
check("마지막 한 마리는 못 내보낸다", not GameState.can_release_fish("hermit_crab"))
|
||||
check("내보내려 해도 아무것도 돌아오지 않다",
|
||||
is_equal_approx(GameState.release_fish("seaweed"), 0.0))
|
||||
is_equal_approx(GameState.release_fish("hermit_crab"), 0.0))
|
||||
check("그대로 남는다", GameState.fish_total == 1, str(GameState.fish_total))
|
||||
|
||||
GameState.buy_fish("bubble_stone")
|
||||
check("둘이 되면 내보낼 수 있다", GameState.can_release_fish("seaweed"))
|
||||
check("실제로 나간다", GameState.release_fish("seaweed") > 0.0)
|
||||
GameState.buy_fish("guppy")
|
||||
check("둘이 되면 내보낼 수 있다", GameState.can_release_fish("hermit_crab"))
|
||||
check("실제로 나간다", GameState.release_fish("hermit_crab") > 0.0)
|
||||
check("한 마리가 남는다", GameState.fish_total == 1, str(GameState.fish_total))
|
||||
check("없는 종류는 못 내보낸다", not GameState.can_release_fish("seaweed"))
|
||||
check("없는 종류는 못 내보낸다", not GameState.can_release_fish("hermit_crab"))
|
||||
|
||||
|
||||
func _test_space_is_the_limit() -> void:
|
||||
@@ -356,8 +362,9 @@ func _test_zone_gating() -> void:
|
||||
GameState.reset(false)
|
||||
GameState.bubbles = 1e15
|
||||
check("시작은 1구역만 열림", GameState.unlocked_zones == 1)
|
||||
check("0구역 물건은 보인다", GameState.is_revealed("producer", "bubble_stone")
|
||||
and GameState.is_revealed("house", "amphora"))
|
||||
# 각 구역의 첫 항목은 값을 벌기 전에도 보여야 한다. 안 그러면 상점이 비어 시작한다.
|
||||
check("0구역 물건은 보인다", GameState.is_revealed("producer", "guppy")
|
||||
and GameState.is_revealed("house", "bubble_stone"))
|
||||
check("1구역 물건은 안 보인다", not GameState.is_revealed("producer", "clownfish")
|
||||
and not GameState.is_revealed("house", "anchor"))
|
||||
check("돈이 넘쳐도 잠긴 구역에는 못 짓는다",
|
||||
@@ -365,7 +372,7 @@ func _test_zone_gating() -> void:
|
||||
check("실패했으면 잔액 그대로", is_equal_approx(GameState.bubbles, 1e15))
|
||||
|
||||
GameState.unlocked_zones = 2
|
||||
check("열린 뒤에는 보인다", GameState.is_revealed("house", "anchor"))
|
||||
check("열린 뒤에는 보인다", GameState.is_revealed("house", "coral"))
|
||||
check("열린 뒤에는 지어진다",
|
||||
GameState.build_house("anchor", Catalog.ZONE_COLUMNS, 0) >= 0)
|
||||
|
||||
@@ -389,7 +396,7 @@ func _test_zone_unlock() -> void:
|
||||
var spot := GameState.first_free_spot("amphora")
|
||||
if spot.x < 0 or GameState.build_house("amphora", spot.x, spot.y) < 0:
|
||||
break
|
||||
elif not GameState.buy_fish("seaweed"):
|
||||
elif not GameState.buy_fish("hermit_crab"):
|
||||
break
|
||||
check("칸 안에서 조건을 채울 수 있다", GameState.beauty_in_zone(0) >= need_beauty,
|
||||
"%f / %f" % [GameState.beauty_in_zone(0), need_beauty])
|
||||
@@ -405,15 +412,15 @@ func _test_zone_beauty_is_per_zone() -> void:
|
||||
print("구역별 아름다움")
|
||||
_rich_and_open()
|
||||
_house_in(0) # 항아리, 아름다움 12
|
||||
GameState.buy_fish("seaweed") # 0구역, 아름다움 3
|
||||
GameState.buy_fish("hermit_crab") # 0구역, 아름다움 4
|
||||
_house_in(1) # 낡은 닻, 아름다움 40
|
||||
GameState.buy_fish("clownfish") # 1구역, 아름다움 8
|
||||
|
||||
check("0구역에 15", is_equal_approx(GameState.beauty_in_zone(0), 15.0),
|
||||
check("0구역에 16", is_equal_approx(GameState.beauty_in_zone(0), 16.0),
|
||||
str(GameState.beauty_in_zone(0)))
|
||||
check("1구역에 48", is_equal_approx(GameState.beauty_in_zone(1), 48.0),
|
||||
str(GameState.beauty_in_zone(1)))
|
||||
check("총합은 63", is_equal_approx(GameState.beauty, 63.0), str(GameState.beauty))
|
||||
check("총합은 64", is_equal_approx(GameState.beauty, 64.0), str(GameState.beauty))
|
||||
check("건드리지 않은 구역은 0", is_equal_approx(GameState.beauty_in_zone(4), 0.0))
|
||||
|
||||
|
||||
@@ -456,8 +463,8 @@ func _test_serialization_round_trip() -> void:
|
||||
_rich_and_open()
|
||||
GameState.build_house("amphora", 1, 1)
|
||||
GameState.build_house("shipwreck", Catalog.ZONE_COLUMNS + 1, 2)
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
GameState.buy_fish("clownfish")
|
||||
GameState.upgrades["u_ripple"] = true
|
||||
# 배치가 끝난 뒤에 잔액을 맞춘다. 짓고 들이는 데 값이 들기 때문이다.
|
||||
@@ -477,7 +484,7 @@ func _test_serialization_round_trip() -> void:
|
||||
GameState.house_at(1, 1) >= 0 and GameState.house_at(wreck, 2) >= 0)
|
||||
check("난파선 발자국도 복원",
|
||||
GameState.house_at(wreck + 2, 3) == GameState.house_at(wreck, 2))
|
||||
check("생물 마릿수 복원", GameState.fish_count("seaweed") == 2
|
||||
check("생물 마릿수 복원", GameState.fish_count("hermit_crab") == 2
|
||||
and GameState.fish_count("clownfish") == 1)
|
||||
check("정원도 그대로", GameState.capacity_in_zone(0) == Catalog.HOUSE_CAPACITY,
|
||||
str(GameState.capacity_in_zone(0)))
|
||||
@@ -491,6 +498,7 @@ func _test_serialization_round_trip() -> void:
|
||||
func _test_legacy_save_migration() -> void:
|
||||
print("옛 저장본 이전")
|
||||
# 생물도 칸을 차지하던 시절. 장식이 지금의 집이 되고, 생물은 마릿수만 남는다.
|
||||
# 돌·해초처럼 그때는 생물이었다가 지금은 집이 된 것은 그만큼 집으로 지어 준다.
|
||||
GameState.reset(false)
|
||||
GameState.from_dict({
|
||||
"bubbles": 500.0,
|
||||
@@ -503,26 +511,32 @@ func _test_legacy_save_migration() -> void:
|
||||
{"kind": "ornament", "id": "anchor", "col": Catalog.ZONE_COLUMNS + 2, "row": 0},
|
||||
],
|
||||
})
|
||||
check("장식이 집이 된다", GameState.houses.size() == 2, str(GameState.houses.size()))
|
||||
# 항아리 + 닻 + 해초 두 포기 = 네 채.
|
||||
check("장식이 집이 된다", GameState.houses.size() == 4, str(GameState.houses.size()))
|
||||
check("집이 선 칸은 그대로", GameState.house_at(3, 0) >= 0)
|
||||
check("생물은 마릿수로 남는다", GameState.fish_count("seaweed") == 2,
|
||||
str(GameState.fish_count("seaweed")))
|
||||
check("생물은 칸을 놓아준다", GameState.house_at(0, 0) == -1)
|
||||
check("각자 제 구역에 남는다", GameState.fish_in_zone(0) == 2
|
||||
and GameState.fish_in_zone(1) == 1)
|
||||
check("해초는 집으로 넘어온다", GameState.house_count("seaweed") == 2,
|
||||
str(GameState.house_count("seaweed")))
|
||||
check("해초는 더 이상 생물이 아니다", GameState.fish_count("seaweed") == 0)
|
||||
check("헤엄치던 것은 마릿수로 남는다", GameState.fish_count("clownfish") == 1,
|
||||
str(GameState.fish_count("clownfish")))
|
||||
check("생물은 칸을 놓아준다", GameState.fish_total == 1, str(GameState.fish_total))
|
||||
check("각자 제 구역에 남는다", GameState.fish_in_zone(1) == 1,
|
||||
str(GameState.fish_in_zone(1)))
|
||||
check("보유량은 그대로", is_equal_approx(GameState.bubbles, 500.0))
|
||||
|
||||
# 정원보다 많이 살고 있으면 집을 세워 다 들어가게 해준다.
|
||||
# 개수만 있던 저장본도 같은 규칙으로 옮겨온다.
|
||||
GameState.reset(false)
|
||||
GameState.from_dict({
|
||||
"bubbles": 1e9,
|
||||
"producers": {"seaweed": 10},
|
||||
"producers": {"seaweed": 10, "guppy": 4},
|
||||
"ornaments": {"amphora": true},
|
||||
})
|
||||
check("옛 개수 저장본도 옮겨온다", GameState.fish_count("seaweed") == 10,
|
||||
str(GameState.fish_count("seaweed")))
|
||||
check("한 마리도 내쫓지 않는다", GameState.fish_total == 10, str(GameState.fish_total))
|
||||
check("정원이 모자라면 집을 더 세워준다", GameState.capacity_in_zone(0) >= 10,
|
||||
check("옛 개수 저장본도 옮겨온다", GameState.house_count("seaweed") == 10,
|
||||
str(GameState.house_count("seaweed")))
|
||||
check("헤엄치던 것은 그대로 마릿수", GameState.fish_count("guppy") == 4,
|
||||
str(GameState.fish_count("guppy")))
|
||||
check("한 마리도 내쫓지 않는다", GameState.fish_total == 4, str(GameState.fish_total))
|
||||
check("정원이 모자라면 집을 더 세워준다", GameState.capacity_in_zone(0) >= 4,
|
||||
"정원 %d / 생물 %d" % [GameState.capacity_in_zone(0), GameState.fish_total])
|
||||
check("보이지 않는 생물이 남지 않는다", GameState.free_slots_in_zone(0) >= 0,
|
||||
str(GameState.free_slots_in_zone(0)))
|
||||
@@ -562,8 +576,9 @@ func _test_shape_families() -> void:
|
||||
print("형태 계통")
|
||||
# 집과 생물이 같은 도형을 쓰면 화면에서 "건물"과 "사는 것"이 구분되지 않는다.
|
||||
# 그래서 두 계통을 아예 겹치지 않게 갈라 두고, 그것을 여기서 지킨다.
|
||||
const CREATURE_SHAPES := ["plant", "rock", "fish", "glow"]
|
||||
const HOUSE_SHAPES := ["vessel", "wreck", "pillar"]
|
||||
# 사는 것은 헤엄치거나 걷는 해양동물, 사는 곳은 자연 서식지와 지어 올린 것.
|
||||
const CREATURE_SHAPES := ["fish", "glow", "crab", "seahorse", "shrimp"]
|
||||
const HOUSE_SHAPES := ["rock", "plant", "vessel", "wreck", "pillar"]
|
||||
|
||||
var bad_creature := ""
|
||||
for d in Catalog.PRODUCERS:
|
||||
@@ -600,6 +615,24 @@ func _test_shape_families() -> void:
|
||||
starter_swims = true
|
||||
check("시작 생물에도 헤엄치는 것이 있다", starter_swims)
|
||||
|
||||
# 고착생물은 전부 집으로 옮겼다. 생물 목록에 다시 섞여 들어오면 안 된다.
|
||||
var sessile := ""
|
||||
for d in Catalog.PRODUCERS:
|
||||
if String(d.shape) in ["plant", "rock"]:
|
||||
sessile = String(d.name)
|
||||
check("생물에 고착생물이 없다", sessile.is_empty(), sessile)
|
||||
|
||||
# 자연 서식지가 하나도 없는 구역이 있으면 값싼 첫 자리가 없어 시작이 막힌다.
|
||||
var starter_home := true
|
||||
for z in Catalog.zone_count():
|
||||
var cheapest := INF
|
||||
for d in Catalog.HOUSES:
|
||||
if int(d.get("zone", 0)) == z:
|
||||
cheapest = minf(cheapest, float(d.base_cost))
|
||||
if not is_finite(cheapest):
|
||||
starter_home = false
|
||||
check("구역마다 지을 집이 있다", starter_home)
|
||||
|
||||
|
||||
func _test_upgrades_are_all_production() -> void:
|
||||
print("업그레이드")
|
||||
@@ -611,7 +644,7 @@ func _test_upgrades_are_all_production() -> void:
|
||||
|
||||
_rich_and_open()
|
||||
_house_in(0)
|
||||
GameState.buy_fish("seaweed")
|
||||
GameState.buy_fish("hermit_crab")
|
||||
var before := GameState.bps
|
||||
var id := String(Catalog.UPGRADES[0].id)
|
||||
check("살 수 있다", GameState.buy_upgrade(id))
|
||||
|
||||
Reference in New Issue
Block a user