test: reformat tests to use double quotes and improve style
This commit is contained in:
@@ -14,7 +14,7 @@ import type {
|
||||
GridInventory,
|
||||
InventoryItem,
|
||||
} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||
import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/encounter/types";
|
||||
import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
||||
import { IDENTITY_TRANSFORM } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
||||
import { parseShapeString } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
||||
import type {
|
||||
|
||||
@@ -1,485 +1,538 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseShapeString } from '@/samples/slay-the-spire-like/system/utils/parse-shape';
|
||||
import { IDENTITY_TRANSFORM } from '@/samples/slay-the-spire-like/system/utils/shape-collision';
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseShapeString } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
||||
import { IDENTITY_TRANSFORM } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
||||
import {
|
||||
createGridInventory,
|
||||
placeItem,
|
||||
removeItem,
|
||||
moveItem,
|
||||
rotateItem,
|
||||
flipItem,
|
||||
getOccupiedCellSet,
|
||||
getItemAtCell,
|
||||
getAdjacentItems,
|
||||
validatePlacement,
|
||||
type GridInventory,
|
||||
type InventoryItem,
|
||||
} from '@/samples/slay-the-spire-like/system/grid-inventory';
|
||||
createGridInventory,
|
||||
placeItem,
|
||||
removeItem,
|
||||
moveItem,
|
||||
rotateItem,
|
||||
flipItem,
|
||||
getOccupiedCellSet,
|
||||
getItemAtCell,
|
||||
getAdjacentItems,
|
||||
validatePlacement,
|
||||
type GridInventory,
|
||||
type InventoryItem,
|
||||
} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||
|
||||
/**
|
||||
* Helper: create a test inventory item.
|
||||
*/
|
||||
function createTestItem(id: string, shapeStr: string, transform = IDENTITY_TRANSFORM): InventoryItem {
|
||||
const shape = parseShapeString(shapeStr);
|
||||
return {
|
||||
id,
|
||||
shape,
|
||||
transform: { ...transform },
|
||||
};
|
||||
function createTestItem(
|
||||
id: string,
|
||||
shapeStr: string,
|
||||
transform = IDENTITY_TRANSFORM,
|
||||
): InventoryItem {
|
||||
const shape = parseShapeString(shapeStr);
|
||||
return {
|
||||
id,
|
||||
shape,
|
||||
transform: { ...transform },
|
||||
};
|
||||
}
|
||||
|
||||
describe('grid-inventory', () => {
|
||||
describe('createGridInventory', () => {
|
||||
it('should create an empty inventory with correct dimensions', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
expect(inv.width).toBe(6);
|
||||
expect(inv.height).toBe(4);
|
||||
expect(inv.items.size).toBe(0);
|
||||
expect(inv.occupiedCells.size).toBe(0);
|
||||
});
|
||||
describe("grid-inventory", () => {
|
||||
describe("createGridInventory", () => {
|
||||
it("should create an empty inventory with correct dimensions", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
expect(inv.width).toBe(6);
|
||||
expect(inv.height).toBe(4);
|
||||
expect(inv.items.size).toBe(0);
|
||||
expect(inv.occupiedCells.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("placeItem", () => {
|
||||
it("should place a single-cell item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "o");
|
||||
placeItem(inv, item);
|
||||
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.items.has("sword")).toBe(true);
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(true);
|
||||
});
|
||||
|
||||
describe('placeItem', () => {
|
||||
it('should place a single-cell item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'o');
|
||||
placeItem(inv, item);
|
||||
it("should place a multi-cell item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("axe", "oee");
|
||||
placeItem(inv, item);
|
||||
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.items.has('sword')).toBe(true);
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should place a multi-cell item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('axe', 'oee');
|
||||
placeItem(inv, item);
|
||||
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.occupiedCells.size).toBe(3);
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(true);
|
||||
expect(inv.occupiedCells.has('1,0')).toBe(true);
|
||||
expect(inv.occupiedCells.has('2,0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should place multiple items', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem('a', 'o');
|
||||
const itemB = createTestItem('b', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 0 } });
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
expect(inv.items.size).toBe(2);
|
||||
expect(inv.occupiedCells.size).toBe(2);
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(true);
|
||||
expect(inv.occupiedCells.has('3,0')).toBe(true);
|
||||
});
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.occupiedCells.size).toBe(3);
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(true);
|
||||
expect(inv.occupiedCells.has("1,0")).toBe(true);
|
||||
expect(inv.occupiedCells.has("2,0")).toBe(true);
|
||||
});
|
||||
|
||||
describe('removeItem', () => {
|
||||
it('should remove an item and free its cells', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'oee');
|
||||
placeItem(inv, item);
|
||||
it("should place multiple items", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem("a", "o");
|
||||
const itemB = createTestItem("b", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 3, y: 0 },
|
||||
});
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
removeItem(inv, 'sword');
|
||||
expect(inv.items.size).toBe(2);
|
||||
expect(inv.occupiedCells.size).toBe(2);
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(true);
|
||||
expect(inv.occupiedCells.has("3,0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
expect(inv.items.size).toBe(0);
|
||||
expect(inv.occupiedCells.size).toBe(0);
|
||||
});
|
||||
describe("removeItem", () => {
|
||||
it("should remove an item and free its cells", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "oee");
|
||||
placeItem(inv, item);
|
||||
|
||||
it('should only free the removed item\'s cells', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem('a', 'o');
|
||||
const itemB = createTestItem('b', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } });
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
removeItem(inv, "sword");
|
||||
|
||||
removeItem(inv, 'a');
|
||||
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.occupiedCells.size).toBe(1);
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(false);
|
||||
expect(inv.occupiedCells.has('2,0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should do nothing for non-existent item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
removeItem(inv, 'nonexistent');
|
||||
expect(inv.items.size).toBe(0);
|
||||
});
|
||||
expect(inv.items.size).toBe(0);
|
||||
expect(inv.occupiedCells.size).toBe(0);
|
||||
});
|
||||
|
||||
describe('validatePlacement', () => {
|
||||
it('should return valid for empty board', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const shape = parseShapeString('o');
|
||||
const result = validatePlacement(inv, shape, IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
it("should only free the removed item's cells", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem("a", "o");
|
||||
const itemB = createTestItem("b", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 0 },
|
||||
});
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
it('should return invalid for out of bounds', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const shape = parseShapeString('o');
|
||||
const result = validatePlacement(inv, shape, {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 6, y: 0 },
|
||||
});
|
||||
expect(result).toEqual({ valid: false, reason: '超出边界' });
|
||||
});
|
||||
removeItem(inv, "a");
|
||||
|
||||
it('should return invalid for collision with existing item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const existing = createTestItem('a', 'oee');
|
||||
placeItem(inv, existing);
|
||||
|
||||
const shape = parseShapeString('o');
|
||||
const result = validatePlacement(inv, shape, IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ valid: false, reason: '与已有物品重叠' });
|
||||
});
|
||||
|
||||
it('should return valid when there is room nearby', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const existing = createTestItem('a', 'o');
|
||||
placeItem(inv, existing);
|
||||
|
||||
const shape = parseShapeString('o');
|
||||
const result = validatePlacement(inv, shape, {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 0 },
|
||||
});
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
expect(inv.items.size).toBe(1);
|
||||
expect(inv.occupiedCells.size).toBe(1);
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(false);
|
||||
expect(inv.occupiedCells.has("2,0")).toBe(true);
|
||||
});
|
||||
|
||||
describe('moveItem', () => {
|
||||
it('should move item to a new position', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'o');
|
||||
placeItem(inv, item);
|
||||
it("should do nothing for non-existent item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
removeItem(inv, "nonexistent");
|
||||
expect(inv.items.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const result = moveItem(inv, 'sword', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 5, y: 3 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(false);
|
||||
expect(inv.occupiedCells.has('5,3')).toBe(true);
|
||||
expect(item.transform.offset).toEqual({ x: 5, y: 3 });
|
||||
});
|
||||
|
||||
it('should reject move that goes out of bounds', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'o');
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = moveItem(inv, 'sword', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 6, y: 0 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, reason: '超出边界' });
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(true);
|
||||
expect(item.transform.offset).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
it('should reject move that collides with another item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem('a', 'o');
|
||||
const itemB = createTestItem('b', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } });
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
const result = moveItem(inv, 'b', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, reason: '与已有物品重叠' });
|
||||
expect(inv.occupiedCells.has('2,0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error for non-existent item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = moveItem(inv, 'ghost', IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ success: false, reason: '物品不存在' });
|
||||
});
|
||||
|
||||
it('should move multi-cell item correctly', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// oes: cells at (0,0), (1,0), (1,1)
|
||||
const item = createTestItem('axe', 'oes');
|
||||
placeItem(inv, item);
|
||||
|
||||
const newTransform = { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 1 } };
|
||||
moveItem(inv, 'axe', newTransform);
|
||||
|
||||
// Old cells should be freed
|
||||
expect(inv.occupiedCells.has('0,0')).toBe(false);
|
||||
expect(inv.occupiedCells.has('1,0')).toBe(false);
|
||||
expect(inv.occupiedCells.has('1,1')).toBe(false);
|
||||
// New cells: (0,0)+offset(3,1)=(3,1), (1,0)+(3,1)=(4,1), (1,1)+(3,1)=(4,2)
|
||||
expect(inv.occupiedCells.has('3,1')).toBe(true);
|
||||
expect(inv.occupiedCells.has('4,1')).toBe(true);
|
||||
expect(inv.occupiedCells.has('4,2')).toBe(true);
|
||||
});
|
||||
describe("validatePlacement", () => {
|
||||
it("should return valid for empty board", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const shape = parseShapeString("o");
|
||||
const result = validatePlacement(inv, shape, IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
describe('rotateItem', () => {
|
||||
it('should rotate item by 90 degrees', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// Horizontal line: (0,0), (1,0)
|
||||
const item = createTestItem('bar', 'oe', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 }, // Place away from edge so rotation stays in bounds
|
||||
});
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = rotateItem(inv, 'bar', 90);
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.rotation).toBe(90);
|
||||
});
|
||||
|
||||
it('should reject rotation that goes out of bounds', () => {
|
||||
const inv = createGridInventory(3, 3);
|
||||
// Item at the edge: place a 2-wide item at x=1
|
||||
const item = createTestItem('bar', 'oe', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 0 },
|
||||
});
|
||||
placeItem(inv, item);
|
||||
|
||||
// Rotating 90° would make it vertical starting at (1,0), going to (1,-1) -> out of bounds
|
||||
const result = rotateItem(inv, 'bar', 90);
|
||||
|
||||
expect(result).toEqual({ success: false, reason: '超出边界' });
|
||||
});
|
||||
|
||||
it('should reject rotation that collides', () => {
|
||||
const inv = createGridInventory(4, 4);
|
||||
const itemA = createTestItem('a', 'o');
|
||||
const itemB = createTestItem('b', 'oe', { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } });
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
// Rotating b 90° would place cells at (2,0) and (2,-1) -> (2,-1) is out of bounds
|
||||
// Let's try a different scenario: rotate b 270° -> (2,0) and (2,1) which is fine
|
||||
// But rotating to collide with a at (0,0)... need item close to a
|
||||
const itemC = createTestItem('c', 'os', { ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } });
|
||||
placeItem(inv, itemC);
|
||||
|
||||
// Rotating c 90° would give cells at (1,0) and (0,0) -> collision with a
|
||||
const result = rotateItem(inv, 'c', 90);
|
||||
expect(result).toEqual({ success: false, reason: '与已有物品重叠' });
|
||||
});
|
||||
|
||||
it('should return error for non-existent item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = rotateItem(inv, 'ghost', 90);
|
||||
expect(result).toEqual({ success: false, reason: '物品不存在' });
|
||||
});
|
||||
it("should return invalid for out of bounds", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const shape = parseShapeString("o");
|
||||
const result = validatePlacement(inv, shape, {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 6, y: 0 },
|
||||
});
|
||||
expect(result).toEqual({ valid: false, reason: "超出边界" });
|
||||
});
|
||||
|
||||
describe('flipItem', () => {
|
||||
it('should flip item horizontally', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('bar', 'oe');
|
||||
placeItem(inv, item);
|
||||
it("should return invalid for collision with existing item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const existing = createTestItem("a", "oee");
|
||||
placeItem(inv, existing);
|
||||
|
||||
const result = flipItem(inv, 'bar', 'x');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.flipX).toBe(true);
|
||||
});
|
||||
|
||||
it('should flip item vertically', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('bar', 'os');
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = flipItem(inv, 'bar', 'y');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.flipY).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject flip that causes collision', () => {
|
||||
// oes local cells: (0,0),(1,0),(1,1). flipY: (0,1),(1,1),(1,0).
|
||||
// Place flipper at offset (0,2): world cells (0,2),(1,2),(1,3).
|
||||
// flipY gives local (0,1),(1,1),(1,0) + offset(0,2) = (0,3),(1,3),(1,2) — same cells rearranged.
|
||||
// Need asymmetric shape where flip changes world position.
|
||||
// Use oes at offset (0,0): cells (0,0),(1,0),(1,1). flipY: (0,1),(1,1),(1,0).
|
||||
// Place blocker at (0,1) — which is NOT occupied by oes initially.
|
||||
const inv = createGridInventory(4, 4);
|
||||
const blocker = createTestItem('blocker', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 1 } });
|
||||
// oes at (0,1): cells (0,1),(1,1),(1,2). This overlaps blocker at (0,1)!
|
||||
// Let me try: blocker at (1,0), flipper at offset (0,2).
|
||||
// flipper oes at (0,2): (0,2),(1,2),(1,3). blocker at (1,0) — no overlap.
|
||||
// flipY: local (0,1),(1,1),(1,0) + offset(0,2) = (0,3),(1,3),(1,2). No collision with (1,0).
|
||||
//
|
||||
// Simpler: oe shape (width=2, height=1). flipY with height=1 is identity. Use os (width=1, height=2).
|
||||
// os: (0,0),(0,1). flipY: (0,1),(0,0) — same cells.
|
||||
// Need width>1 and height>1 asymmetric shape: oes
|
||||
//
|
||||
// Place flipper at (0,0): cells (0,0),(1,0),(1,1). Place blocker at (0,1) — but (0,1) is not occupied.
|
||||
// flipY: (0,1),(1,1),(1,0). (0,1) hits blocker!
|
||||
const inv2 = createGridInventory(4, 4);
|
||||
const blocker2 = createTestItem('blocker', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 1 } });
|
||||
const flipper2 = createTestItem('flipper', 'oes'); // at (0,0): (0,0),(1,0),(1,1)
|
||||
placeItem(inv2, blocker2);
|
||||
placeItem(inv2, flipper2);
|
||||
|
||||
const result = flipItem(inv2, 'flipper', 'y');
|
||||
expect(result).toEqual({ success: false, reason: '与已有物品重叠' });
|
||||
});
|
||||
|
||||
it('should return error for non-existent item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = flipItem(inv, 'ghost', 'x');
|
||||
expect(result).toEqual({ success: false, reason: '物品不存在' });
|
||||
});
|
||||
const shape = parseShapeString("o");
|
||||
const result = validatePlacement(inv, shape, IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ valid: false, reason: "与已有物品重叠" });
|
||||
});
|
||||
|
||||
describe('getOccupiedCellSet', () => {
|
||||
it('should return a copy of occupied cells', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('a', 'oe');
|
||||
placeItem(inv, item);
|
||||
it("should return valid when there is room nearby", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const existing = createTestItem("a", "o");
|
||||
placeItem(inv, existing);
|
||||
|
||||
const cells = getOccupiedCellSet(inv);
|
||||
expect(cells).toEqual(new Set(['0,0', '1,0']));
|
||||
const shape = parseShapeString("o");
|
||||
const result = validatePlacement(inv, shape, {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 0 },
|
||||
});
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Mutating the copy should not affect the original
|
||||
cells.clear();
|
||||
expect(inv.occupiedCells.size).toBe(2);
|
||||
});
|
||||
describe("moveItem", () => {
|
||||
it("should move item to a new position", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "o");
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = moveItem(inv, "sword", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 5, y: 3 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(false);
|
||||
expect(inv.occupiedCells.has("5,3")).toBe(true);
|
||||
expect(item.transform.offset).toEqual({ x: 5, y: 3 });
|
||||
});
|
||||
|
||||
describe('getItemAtCell', () => {
|
||||
it('should return item at occupied cell', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'oee');
|
||||
placeItem(inv, item);
|
||||
it("should reject move that goes out of bounds", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "o");
|
||||
placeItem(inv, item);
|
||||
|
||||
const found = getItemAtCell(inv, 1, 0);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.id).toBe('sword');
|
||||
});
|
||||
const result = moveItem(inv, "sword", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 6, y: 0 },
|
||||
});
|
||||
|
||||
it('should return undefined for empty cell', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('sword', 'o');
|
||||
placeItem(inv, item);
|
||||
|
||||
const found = getItemAtCell(inv, 5, 5);
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return correct item when multiple items exist', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem('a', 'o');
|
||||
const itemB = createTestItem('b', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 2 } });
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
expect(getItemAtCell(inv, 0, 0)!.id).toBe('a');
|
||||
expect(getItemAtCell(inv, 3, 2)!.id).toBe('b');
|
||||
});
|
||||
expect(result).toEqual({ success: false, reason: "超出边界" });
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(true);
|
||||
expect(item.transform.offset).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
describe('getAdjacentItems', () => {
|
||||
it('should return orthogonally adjacent items', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const center = createTestItem('center', 'o', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 2 },
|
||||
});
|
||||
const top = createTestItem('top', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 1 } });
|
||||
const left = createTestItem('left', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 1, y: 2 } });
|
||||
const right = createTestItem('right', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 2 } });
|
||||
const bottom = createTestItem('bottom', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 3 } });
|
||||
const diagonal = createTestItem('diagonal', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 1, y: 1 } });
|
||||
it("should reject move that collides with another item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem("a", "o");
|
||||
const itemB = createTestItem("b", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 0 },
|
||||
});
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
placeItem(inv, center);
|
||||
placeItem(inv, top);
|
||||
placeItem(inv, left);
|
||||
placeItem(inv, right);
|
||||
placeItem(inv, bottom);
|
||||
placeItem(inv, diagonal);
|
||||
const result = moveItem(inv, "b", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
const adj = getAdjacentItems(inv, 'center');
|
||||
expect(adj.size).toBe(4);
|
||||
expect(adj.has('top')).toBe(true);
|
||||
expect(adj.has('left')).toBe(true);
|
||||
expect(adj.has('right')).toBe(true);
|
||||
expect(adj.has('bottom')).toBe(true);
|
||||
expect(adj.has('diagonal')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return empty for item with no neighbors', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem('alone', 'o');
|
||||
placeItem(inv, item);
|
||||
|
||||
const adj = getAdjacentItems(inv, 'alone');
|
||||
expect(adj.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return empty for non-existent item', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const adj = getAdjacentItems(inv, 'ghost');
|
||||
expect(adj.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle multi-cell items with multiple adjacencies', () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// Horizontal bar at (0,0)-(1,0)
|
||||
const bar = createTestItem('bar', 'oe');
|
||||
// Item above left cell
|
||||
const topA = createTestItem('topA', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 0, y: -1 } });
|
||||
// Item above right cell
|
||||
const topB = createTestItem('topB', 'o', { ...IDENTITY_TRANSFORM, offset: { x: 1, y: -1 } });
|
||||
|
||||
placeItem(inv, bar);
|
||||
placeItem(inv, topA);
|
||||
placeItem(inv, topB);
|
||||
|
||||
const adj = getAdjacentItems(inv, 'bar');
|
||||
expect(adj.size).toBe(2);
|
||||
expect(adj.has('topA')).toBe(true);
|
||||
expect(adj.has('topB')).toBe(true);
|
||||
});
|
||||
expect(result).toEqual({ success: false, reason: "与已有物品重叠" });
|
||||
expect(inv.occupiedCells.has("2,0")).toBe(true);
|
||||
});
|
||||
|
||||
describe('integration: fill a 4x6 backpack', () => {
|
||||
it('should place items fitting a slay-the-spire-like backpack', () => {
|
||||
const inv = createGridInventory(4, 6);
|
||||
|
||||
// Sword: 1x3 horizontal at (0,0)
|
||||
const sword = createTestItem('sword', 'oee');
|
||||
// Shield: 2x2 at (0,1)
|
||||
const shield = createTestItem('shield', 'oes', {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 },
|
||||
});
|
||||
|
||||
expect(validatePlacement(inv, sword.shape, sword.transform)).toEqual({ valid: true });
|
||||
placeItem(inv, sword);
|
||||
|
||||
expect(validatePlacement(inv, shield.shape, shield.transform)).toEqual({ valid: true });
|
||||
placeItem(inv, shield);
|
||||
|
||||
expect(inv.items.size).toBe(2);
|
||||
expect(inv.occupiedCells.size).toBe(6); // sword(3) + shield(3)
|
||||
|
||||
// Adjacent items should detect each other
|
||||
const adjSword = getAdjacentItems(inv, 'sword');
|
||||
expect(adjSword.has('shield')).toBe(true);
|
||||
|
||||
const adjShield = getAdjacentItems(inv, 'shield');
|
||||
expect(adjShield.has('sword')).toBe(true);
|
||||
});
|
||||
it("should return error for non-existent item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = moveItem(inv, "ghost", IDENTITY_TRANSFORM);
|
||||
expect(result).toEqual({ success: false, reason: "物品不存在" });
|
||||
});
|
||||
|
||||
it("should move multi-cell item correctly", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// oes: cells at (0,0), (1,0), (1,1)
|
||||
const item = createTestItem("axe", "oes");
|
||||
placeItem(inv, item);
|
||||
|
||||
const newTransform = { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 1 } };
|
||||
moveItem(inv, "axe", newTransform);
|
||||
|
||||
// Old cells should be freed
|
||||
expect(inv.occupiedCells.has("0,0")).toBe(false);
|
||||
expect(inv.occupiedCells.has("1,0")).toBe(false);
|
||||
expect(inv.occupiedCells.has("1,1")).toBe(false);
|
||||
// New cells: (0,0)+offset(3,1)=(3,1), (1,0)+(3,1)=(4,1), (1,1)+(3,1)=(4,2)
|
||||
expect(inv.occupiedCells.has("3,1")).toBe(true);
|
||||
expect(inv.occupiedCells.has("4,1")).toBe(true);
|
||||
expect(inv.occupiedCells.has("4,2")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateItem", () => {
|
||||
it("should rotate item by 90 degrees", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// Horizontal line: (0,0), (1,0)
|
||||
const item = createTestItem("bar", "oe", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 }, // Place away from edge so rotation stays in bounds
|
||||
});
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = rotateItem(inv, "bar", 90);
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.rotation).toBe(90);
|
||||
});
|
||||
|
||||
it("should reject rotation that goes out of bounds", () => {
|
||||
const inv = createGridInventory(3, 3);
|
||||
// Item at the edge: place a 2-wide item at x=1
|
||||
const item = createTestItem("bar", "oe", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 0 },
|
||||
});
|
||||
placeItem(inv, item);
|
||||
|
||||
// Rotating 90° would make it vertical starting at (1,0), going to (1,-1) -> out of bounds
|
||||
const result = rotateItem(inv, "bar", 90);
|
||||
|
||||
expect(result).toEqual({ success: false, reason: "超出边界" });
|
||||
});
|
||||
|
||||
it("should reject rotation that collides", () => {
|
||||
const inv = createGridInventory(4, 4);
|
||||
const itemA = createTestItem("a", "o");
|
||||
const itemB = createTestItem("b", "oe", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 0 },
|
||||
});
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
// Rotating b 90° would place cells at (2,0) and (2,-1) -> (2,-1) is out of bounds
|
||||
// Let's try a different scenario: rotate b 270° -> (2,0) and (2,1) which is fine
|
||||
// But rotating to collide with a at (0,0)... need item close to a
|
||||
const itemC = createTestItem("c", "os", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 0 },
|
||||
});
|
||||
placeItem(inv, itemC);
|
||||
|
||||
// Rotating c 90° would give cells at (1,0) and (0,0) -> collision with a
|
||||
const result = rotateItem(inv, "c", 90);
|
||||
expect(result).toEqual({ success: false, reason: "与已有物品重叠" });
|
||||
});
|
||||
|
||||
it("should return error for non-existent item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = rotateItem(inv, "ghost", 90);
|
||||
expect(result).toEqual({ success: false, reason: "物品不存在" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("flipItem", () => {
|
||||
it("should flip item horizontally", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("bar", "oe");
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = flipItem(inv, "bar", "x");
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.flipX).toBe(true);
|
||||
});
|
||||
|
||||
it("should flip item vertically", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("bar", "os");
|
||||
placeItem(inv, item);
|
||||
|
||||
const result = flipItem(inv, "bar", "y");
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(item.transform.flipY).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject flip that causes collision", () => {
|
||||
// oes local cells: (0,0),(1,0),(1,1). flipY: (0,1),(1,1),(1,0).
|
||||
// Place flipper at offset (0,2): world cells (0,2),(1,2),(1,3).
|
||||
// flipY gives local (0,1),(1,1),(1,0) + offset(0,2) = (0,3),(1,3),(1,2) — same cells rearranged.
|
||||
// Need asymmetric shape where flip changes world position.
|
||||
// Use oes at offset (0,0): cells (0,0),(1,0),(1,1). flipY: (0,1),(1,1),(1,0).
|
||||
// Place blocker at (0,1) — which is NOT occupied by oes initially.
|
||||
const inv = createGridInventory(4, 4);
|
||||
const blocker = createTestItem("blocker", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 },
|
||||
});
|
||||
// oes at (0,1): cells (0,1),(1,1),(1,2). This overlaps blocker at (0,1)!
|
||||
// Let me try: blocker at (1,0), flipper at offset (0,2).
|
||||
// flipper oes at (0,2): (0,2),(1,2),(1,3). blocker at (1,0) — no overlap.
|
||||
// flipY: local (0,1),(1,1),(1,0) + offset(0,2) = (0,3),(1,3),(1,2). No collision with (1,0).
|
||||
//
|
||||
// Simpler: oe shape (width=2, height=1). flipY with height=1 is identity. Use os (width=1, height=2).
|
||||
// os: (0,0),(0,1). flipY: (0,1),(0,0) — same cells.
|
||||
// Need width>1 and height>1 asymmetric shape: oes
|
||||
//
|
||||
// Place flipper at (0,0): cells (0,0),(1,0),(1,1). Place blocker at (0,1) — but (0,1) is not occupied.
|
||||
// flipY: (0,1),(1,1),(1,0). (0,1) hits blocker!
|
||||
const inv2 = createGridInventory(4, 4);
|
||||
const blocker2 = createTestItem("blocker", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 },
|
||||
});
|
||||
const flipper2 = createTestItem("flipper", "oes"); // at (0,0): (0,0),(1,0),(1,1)
|
||||
placeItem(inv2, blocker2);
|
||||
placeItem(inv2, flipper2);
|
||||
|
||||
const result = flipItem(inv2, "flipper", "y");
|
||||
expect(result).toEqual({ success: false, reason: "与已有物品重叠" });
|
||||
});
|
||||
|
||||
it("should return error for non-existent item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const result = flipItem(inv, "ghost", "x");
|
||||
expect(result).toEqual({ success: false, reason: "物品不存在" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOccupiedCellSet", () => {
|
||||
it("should return a copy of occupied cells", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("a", "oe");
|
||||
placeItem(inv, item);
|
||||
|
||||
const cells = getOccupiedCellSet(inv);
|
||||
expect(cells).toEqual(new Set(["0,0", "1,0"]));
|
||||
|
||||
// Mutating the copy should not affect the original
|
||||
cells.clear();
|
||||
expect(inv.occupiedCells.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getItemAtCell", () => {
|
||||
it("should return item at occupied cell", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "oee");
|
||||
placeItem(inv, item);
|
||||
|
||||
const found = getItemAtCell(inv, 1, 0);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.id).toBe("sword");
|
||||
});
|
||||
|
||||
it("should return undefined for empty cell", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("sword", "o");
|
||||
placeItem(inv, item);
|
||||
|
||||
const found = getItemAtCell(inv, 5, 5);
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return correct item when multiple items exist", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const itemA = createTestItem("a", "o");
|
||||
const itemB = createTestItem("b", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 3, y: 2 },
|
||||
});
|
||||
placeItem(inv, itemA);
|
||||
placeItem(inv, itemB);
|
||||
|
||||
expect(getItemAtCell(inv, 0, 0)!.id).toBe("a");
|
||||
expect(getItemAtCell(inv, 3, 2)!.id).toBe("b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAdjacentItems", () => {
|
||||
it("should return orthogonally adjacent items", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const center = createTestItem("center", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 2 },
|
||||
});
|
||||
const top = createTestItem("top", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 1 },
|
||||
});
|
||||
const left = createTestItem("left", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 2 },
|
||||
});
|
||||
const right = createTestItem("right", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 3, y: 2 },
|
||||
});
|
||||
const bottom = createTestItem("bottom", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 2, y: 3 },
|
||||
});
|
||||
const diagonal = createTestItem("diagonal", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: 1 },
|
||||
});
|
||||
|
||||
placeItem(inv, center);
|
||||
placeItem(inv, top);
|
||||
placeItem(inv, left);
|
||||
placeItem(inv, right);
|
||||
placeItem(inv, bottom);
|
||||
placeItem(inv, diagonal);
|
||||
|
||||
const adj = getAdjacentItems(inv, "center");
|
||||
expect(adj.size).toBe(4);
|
||||
expect(adj.has("top")).toBe(true);
|
||||
expect(adj.has("left")).toBe(true);
|
||||
expect(adj.has("right")).toBe(true);
|
||||
expect(adj.has("bottom")).toBe(true);
|
||||
expect(adj.has("diagonal")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return empty for item with no neighbors", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const item = createTestItem("alone", "o");
|
||||
placeItem(inv, item);
|
||||
|
||||
const adj = getAdjacentItems(inv, "alone");
|
||||
expect(adj.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should return empty for non-existent item", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
const adj = getAdjacentItems(inv, "ghost");
|
||||
expect(adj.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle multi-cell items with multiple adjacencies", () => {
|
||||
const inv = createGridInventory(6, 4);
|
||||
// Horizontal bar at (0,0)-(1,0)
|
||||
const bar = createTestItem("bar", "oe");
|
||||
// Item above left cell
|
||||
const topA = createTestItem("topA", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: -1 },
|
||||
});
|
||||
// Item above right cell
|
||||
const topB = createTestItem("topB", "o", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 1, y: -1 },
|
||||
});
|
||||
|
||||
placeItem(inv, bar);
|
||||
placeItem(inv, topA);
|
||||
placeItem(inv, topB);
|
||||
|
||||
const adj = getAdjacentItems(inv, "bar");
|
||||
expect(adj.size).toBe(2);
|
||||
expect(adj.has("topA")).toBe(true);
|
||||
expect(adj.has("topB")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration: fill a 4x6 backpack", () => {
|
||||
it("should place items fitting a slay-the-spire-like backpack", () => {
|
||||
const inv = createGridInventory(4, 6);
|
||||
|
||||
// Sword: 1x3 horizontal at (0,0)
|
||||
const sword = createTestItem("sword", "oee");
|
||||
// Shield: 2x2 at (0,1)
|
||||
const shield = createTestItem("shield", "oes", {
|
||||
...IDENTITY_TRANSFORM,
|
||||
offset: { x: 0, y: 1 },
|
||||
});
|
||||
|
||||
expect(validatePlacement(inv, sword.shape, sword.transform)).toEqual({
|
||||
valid: true,
|
||||
});
|
||||
placeItem(inv, sword);
|
||||
|
||||
expect(validatePlacement(inv, shield.shape, shield.transform)).toEqual({
|
||||
valid: true,
|
||||
});
|
||||
placeItem(inv, shield);
|
||||
|
||||
expect(inv.items.size).toBe(2);
|
||||
expect(inv.occupiedCells.size).toBe(6); // sword(3) + shield(3)
|
||||
|
||||
// Adjacent items should detect each other
|
||||
const adjSword = getAdjacentItems(inv, "sword");
|
||||
expect(adjSword.has("shield")).toBe(true);
|
||||
|
||||
const adjShield = getAdjacentItems(inv, "shield");
|
||||
expect(adjShield.has("sword")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user