feat(harvest): add bounded crop traversal and drops
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-003: Harvest connected mature crops"
|
title: "US-003: Harvest connected mature crops"
|
||||||
description: Animate bounded harvesting across connected mature crops when a player performs a normal crop break.
|
description: Animate bounded harvesting across connected mature crops when a player performs a normal crop break.
|
||||||
status: backlog
|
status: in-progress
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-003: Harvest connected mature crops
|
# US-003: Harvest connected mature crops
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-004: Replant crops and distribute drops"
|
title: "US-004: Replant crops and distribute drops"
|
||||||
description: Automatically replant harvested crops and deliver their remaining drops safely to the player.
|
description: Automatically replant harvested crops and deliver their remaining drops safely to the player.
|
||||||
status: backlog
|
status: in-progress
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-004: Replant crops and distribute drops
|
# US-004: Replant crops and distribute drops
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package games.dmg.spigotharvest;
|
||||||
|
|
||||||
|
/** World-local integer block coordinates. */
|
||||||
|
public record BlockPosition(int x, int y, int z) {
|
||||||
|
public BlockPosition offset(int deltaX, int deltaY, int deltaZ) {
|
||||||
|
return new BlockPosition(x + deltaX, y + deltaY, z + deltaZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package games.dmg.spigotharvest;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
/** Deterministic bounded search over diagonally and gradually connected crops. */
|
||||||
|
public final class ConnectedCropSearch {
|
||||||
|
private ConnectedCropSearch() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<BlockPosition> find(
|
||||||
|
BlockPosition origin,
|
||||||
|
Predicate<BlockPosition> eligible,
|
||||||
|
int limit
|
||||||
|
) {
|
||||||
|
if (limit <= 0 || !eligible.test(origin)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BlockPosition> result = new ArrayList<>(limit);
|
||||||
|
Queue<BlockPosition> queue = new ArrayDeque<>();
|
||||||
|
Set<BlockPosition> visited = new HashSet<>();
|
||||||
|
queue.add(origin);
|
||||||
|
visited.add(origin);
|
||||||
|
while (!queue.isEmpty() && result.size() < limit) {
|
||||||
|
BlockPosition current = queue.remove();
|
||||||
|
if (!eligible.test(current)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(current);
|
||||||
|
enqueueNeighbors(current, queue, visited);
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void enqueueNeighbors(
|
||||||
|
BlockPosition position,
|
||||||
|
Queue<BlockPosition> queue,
|
||||||
|
Set<BlockPosition> visited
|
||||||
|
) {
|
||||||
|
for (int deltaY = -1; deltaY <= 1; deltaY++) {
|
||||||
|
for (int deltaX = -1; deltaX <= 1; deltaX++) {
|
||||||
|
for (int deltaZ = -1; deltaZ <= 1; deltaZ++) {
|
||||||
|
if (deltaX == 0 && deltaZ == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockPosition neighbor = position.offset(deltaX, deltaY, deltaZ);
|
||||||
|
if (visited.add(neighbor)) {
|
||||||
|
queue.add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package games.dmg.spigotharvest;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
/** Drop result after reserving exactly one item for replanting. */
|
||||||
|
public record CropDropPlan(boolean canReplant, List<ItemStack> remainingDrops) {
|
||||||
|
public CropDropPlan {
|
||||||
|
remainingDrops = List.copyOf(remainingDrops);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CropDropPlan create(CropType crop, Collection<ItemStack> generatedDrops) {
|
||||||
|
List<ItemStack> drops = new ArrayList<>();
|
||||||
|
boolean consumed = false;
|
||||||
|
for (ItemStack generated : generatedDrops) {
|
||||||
|
ItemStack item = generated.clone();
|
||||||
|
if (!consumed && item.getType() == crop.plantingMaterial() && item.getAmount() > 0) {
|
||||||
|
item.setAmount(item.getAmount() - 1);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
if (item.getAmount() > 0) {
|
||||||
|
drops.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return consumed ? new CropDropPlan(true, drops) : new CropDropPlan(false, List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package games.dmg.spigotharvest;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class ConnectedCropSearchTest {
|
||||||
|
@Test
|
||||||
|
void followsEightDirectionsAndOneVerticalBlockPerConnection() {
|
||||||
|
BlockPosition origin = new BlockPosition(0, 64, 0);
|
||||||
|
Set<BlockPosition> mature = Set.of(
|
||||||
|
origin,
|
||||||
|
new BlockPosition(1, 65, 1),
|
||||||
|
new BlockPosition(2, 66, 2),
|
||||||
|
new BlockPosition(3, 68, 3),
|
||||||
|
new BlockPosition(-1, 63, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<BlockPosition> result = ConnectedCropSearch.find(origin, mature::contains, 10);
|
||||||
|
|
||||||
|
assertEquals(4, result.size());
|
||||||
|
assertTrue(result.contains(new BlockPosition(2, 66, 2)));
|
||||||
|
assertTrue(!result.contains(new BlockPosition(3, 68, 3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDeterministicUniqueAndBounded() {
|
||||||
|
BlockPosition origin = new BlockPosition(0, 64, 0);
|
||||||
|
List<BlockPosition> first = ConnectedCropSearch.find(origin, ignored -> true, 4);
|
||||||
|
List<BlockPosition> second = ConnectedCropSearch.find(origin, ignored -> true, 4);
|
||||||
|
|
||||||
|
assertEquals(first, second);
|
||||||
|
assertEquals(4, Set.copyOf(first).size());
|
||||||
|
assertEquals(origin, first.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package games.dmg.spigotharvest;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class CropDropPlanTest {
|
||||||
|
@Test
|
||||||
|
void consumesExactlyOnePlantingItemAndKeepsRemainingDrops() {
|
||||||
|
CropDropPlan plan = CropDropPlan.create(
|
||||||
|
CropType.WHEAT,
|
||||||
|
List.of(new ItemStack(Material.WHEAT, 1), new ItemStack(Material.WHEAT_SEEDS, 3))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(plan.canReplant());
|
||||||
|
assertEquals(2, plan.remainingDrops().stream()
|
||||||
|
.filter(item -> item.getType() == Material.WHEAT_SEEDS)
|
||||||
|
.mapToInt(ItemStack::getAmount).sum());
|
||||||
|
assertEquals(1, plan.remainingDrops().stream()
|
||||||
|
.filter(item -> item.getType() == Material.WHEAT)
|
||||||
|
.mapToInt(ItemStack::getAmount).sum());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refusesHarvestWhenDropsCannotReplant() {
|
||||||
|
CropDropPlan plan = CropDropPlan.create(
|
||||||
|
CropType.BEETROOT,
|
||||||
|
List.of(new ItemStack(Material.BEETROOT, 2))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(!plan.canReplant());
|
||||||
|
assertTrue(plan.remainingDrops().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user