368 lines
10 KiB
TypeScript
368 lines
10 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { parseShapeString } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
|
import {
|
|
checkCollision,
|
|
checkBoardCollision,
|
|
checkBounds,
|
|
validateShapePlacement,
|
|
transformShape,
|
|
getOccupiedCells,
|
|
IDENTITY_TRANSFORM,
|
|
} from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
|
|
|
describe("parseShapeString", () => {
|
|
it("should parse a single cell with o", () => {
|
|
const result = parseShapeString("o");
|
|
expect(result.grid).toEqual([[true]]);
|
|
expect(result.width).toBe(1);
|
|
expect(result.height).toBe(1);
|
|
expect(result.count).toBe(1);
|
|
expect(result.originX).toBe(0);
|
|
expect(result.originY).toBe(0);
|
|
});
|
|
|
|
it("should parse a horizontal line", () => {
|
|
const result = parseShapeString("oee");
|
|
expect(result.width).toBe(3);
|
|
expect(result.height).toBe(1);
|
|
expect(result.count).toBe(3);
|
|
expect(result.grid).toEqual([[true, true, true]]);
|
|
expect(result.originX).toBe(0);
|
|
expect(result.originY).toBe(0);
|
|
});
|
|
|
|
it("should parse a vertical line", () => {
|
|
const result = parseShapeString("oss");
|
|
expect(result.width).toBe(1);
|
|
expect(result.height).toBe(3);
|
|
expect(result.count).toBe(3);
|
|
expect(result.grid).toEqual([[true], [true], [true]]);
|
|
expect(result.originX).toBe(0);
|
|
expect(result.originY).toBe(0);
|
|
});
|
|
|
|
it("should parse an L shape", () => {
|
|
const result = parseShapeString("oes");
|
|
expect(result.width).toBe(2);
|
|
expect(result.height).toBe(2);
|
|
expect(result.count).toBe(3);
|
|
expect(result.grid).toEqual([
|
|
[true, true],
|
|
[false, true],
|
|
]);
|
|
});
|
|
|
|
it("should handle return command", () => {
|
|
const result = parseShapeString("oeerww");
|
|
expect(result.width).toBe(4);
|
|
expect(result.height).toBe(1);
|
|
expect(result.count).toBe(4);
|
|
expect(result.grid).toEqual([[true, true, true, true]]);
|
|
});
|
|
|
|
it("should handle case insensitivity", () => {
|
|
const resultLower = parseShapeString("oes");
|
|
const resultUpper = parseShapeString("OES");
|
|
expect(resultLower.grid).toEqual(resultUpper.grid);
|
|
expect(resultLower.count).toBe(resultUpper.count);
|
|
});
|
|
|
|
it("should return empty grid for empty input", () => {
|
|
const result = parseShapeString("");
|
|
expect(result.grid).toEqual([[]]);
|
|
expect(result.width).toBe(0);
|
|
expect(result.height).toBe(1);
|
|
expect(result.count).toBe(0);
|
|
});
|
|
|
|
it("should track origin correctly", () => {
|
|
// eeso: e(1,0), e(2,0), s(2,1), o sets origin at (2,1)
|
|
// After normalization: minX=1, minY=0, so originX = 2-1 = 1, originY = 1-0 = 1
|
|
const result = parseShapeString("eeso");
|
|
expect(result.originX).toBe(1);
|
|
expect(result.originY).toBe(1);
|
|
});
|
|
|
|
it("should track origin at first o only", () => {
|
|
const result = parseShapeString("oes");
|
|
expect(result.originX).toBe(0);
|
|
expect(result.originY).toBe(0);
|
|
});
|
|
|
|
it("should handle complex T shape", () => {
|
|
// oewers: o(0,0), e(1,0), w(0,0), e(1,0), r->(0,0), s(0,1)
|
|
// Filled: (0,0), (1,0), (0,1) - 3 cells
|
|
const result = parseShapeString("oewers");
|
|
expect(result.width).toBe(2);
|
|
expect(result.height).toBe(2);
|
|
expect(result.count).toBe(3);
|
|
expect(result.grid).toEqual([
|
|
[true, true],
|
|
[true, false],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("shape-collision", () => {
|
|
describe("getOccupiedCells", () => {
|
|
it("should return cells for a single cell shape", () => {
|
|
const shape = parseShapeString("o");
|
|
const cells = getOccupiedCells(shape);
|
|
expect(cells).toEqual([{ x: 0, y: 0 }]);
|
|
});
|
|
|
|
it("should return cells for a horizontal line", () => {
|
|
const shape = parseShapeString("oe");
|
|
const cells = getOccupiedCells(shape);
|
|
expect(cells).toEqual([
|
|
{ x: 0, y: 0 },
|
|
{ x: 1, y: 0 },
|
|
]);
|
|
});
|
|
|
|
it("should return cells for an L shape", () => {
|
|
const shape = parseShapeString("oes");
|
|
const cells = getOccupiedCells(shape);
|
|
expect(cells).toEqual([
|
|
{ x: 0, y: 0 },
|
|
{ x: 1, y: 0 },
|
|
{ x: 1, y: 1 },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("checkCollision", () => {
|
|
it("should detect collision between overlapping shapes", () => {
|
|
const shapeA = parseShapeString("o");
|
|
const shapeB = parseShapeString("o");
|
|
|
|
const result = checkCollision(
|
|
shapeA,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
shapeB,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should not detect collision between non-overlapping shapes", () => {
|
|
const shapeA = parseShapeString("o");
|
|
const shapeB = parseShapeString("o");
|
|
|
|
const result = checkCollision(
|
|
shapeA,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
shapeB,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } },
|
|
);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should detect collision with adjacent shapes", () => {
|
|
const shapeA = parseShapeString("o");
|
|
const shapeB = parseShapeString("o");
|
|
|
|
const result = checkCollision(
|
|
shapeA,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
shapeB,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } },
|
|
);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should detect collision with rotation", () => {
|
|
const shapeA = parseShapeString("oe");
|
|
const shapeB = parseShapeString("os");
|
|
|
|
// shapeA is horizontal at (0,0)-(1,0)
|
|
// shapeB rotated 90° becomes vertical at (0,0)-(0,1)
|
|
// They should collide at (0,0)
|
|
const result = checkCollision(
|
|
shapeA,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
shapeB,
|
|
{ ...IDENTITY_TRANSFORM, rotation: 90, offset: { x: 0, y: 0 } },
|
|
);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("checkBoardCollision", () => {
|
|
it("should detect collision with occupied cells", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set(["0,0", "1,0"]);
|
|
|
|
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should not detect collision with empty board", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set<string>();
|
|
|
|
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should detect collision after translation", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set(["5,5", "6,5"]);
|
|
|
|
const result = checkBoardCollision(
|
|
shape,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 5, y: 5 } },
|
|
occupied,
|
|
);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("checkBounds", () => {
|
|
it("should return true for shape within bounds", () => {
|
|
const shape = parseShapeString("oe");
|
|
|
|
const result = checkBounds(shape, IDENTITY_TRANSFORM, 10, 10);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should return false for shape outside bounds", () => {
|
|
const shape = parseShapeString("oe");
|
|
|
|
const result = checkBounds(
|
|
shape,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
|
10,
|
|
10,
|
|
);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should return false for negative coordinates", () => {
|
|
const shape = parseShapeString("oe");
|
|
|
|
const result = checkBounds(
|
|
shape,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: -1, y: 0 } },
|
|
10,
|
|
10,
|
|
);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should return true for shape at boundary edge", () => {
|
|
const shape = parseShapeString("o");
|
|
|
|
const result = checkBounds(
|
|
shape,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 9 } },
|
|
10,
|
|
10,
|
|
);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validatePlacement", () => {
|
|
it("should return valid for good placement", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set<string>();
|
|
|
|
const result = validateShapePlacement(
|
|
shape,
|
|
IDENTITY_TRANSFORM,
|
|
10,
|
|
10,
|
|
occupied,
|
|
);
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
|
|
it("should return invalid for out of bounds", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set<string>();
|
|
|
|
const result = validateShapePlacement(
|
|
shape,
|
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
|
10,
|
|
10,
|
|
occupied,
|
|
);
|
|
expect(result).toEqual({ valid: false, reason: "超出边界" });
|
|
});
|
|
|
|
it("should return invalid for collision", () => {
|
|
const shape = parseShapeString("oe");
|
|
const occupied = new Set(["0,0", "1,0"]);
|
|
|
|
const result = validateShapePlacement(
|
|
shape,
|
|
IDENTITY_TRANSFORM,
|
|
10,
|
|
10,
|
|
occupied,
|
|
);
|
|
expect(result).toEqual({ valid: false, reason: "与已有形状重叠" });
|
|
});
|
|
});
|
|
|
|
describe("transformShape", () => {
|
|
it("should apply translation correctly", () => {
|
|
const shape = parseShapeString("o");
|
|
const transform = { ...IDENTITY_TRANSFORM, offset: { x: 5, y: 3 } };
|
|
|
|
const cells = transformShape(shape, transform);
|
|
expect(cells).toEqual([{ x: 5, y: 3 }]);
|
|
});
|
|
|
|
it("should apply 90° rotation correctly", () => {
|
|
const shape = parseShapeString("oe");
|
|
const transform = { ...IDENTITY_TRANSFORM, rotation: 90 };
|
|
|
|
const cells = transformShape(shape, transform);
|
|
expect(cells).toEqual([
|
|
{ x: 0, y: 0 },
|
|
{ x: 0, y: -1 },
|
|
]);
|
|
});
|
|
|
|
it("should apply horizontal flip correctly", () => {
|
|
const shape = parseShapeString("oe");
|
|
const transform = { ...IDENTITY_TRANSFORM, flipX: true };
|
|
|
|
const cells = transformShape(shape, transform);
|
|
expect(cells).toEqual([
|
|
{ x: 1, y: 0 },
|
|
{ x: 0, y: 0 },
|
|
]);
|
|
});
|
|
|
|
it("should apply vertical flip correctly", () => {
|
|
const shape = parseShapeString("os");
|
|
const transform = { ...IDENTITY_TRANSFORM, flipY: true };
|
|
|
|
const cells = transformShape(shape, transform);
|
|
expect(cells).toEqual([
|
|
{ x: 0, y: 1 },
|
|
{ x: 0, y: 0 },
|
|
]);
|
|
});
|
|
|
|
it("should combine rotation and translation", () => {
|
|
const shape = parseShapeString("os");
|
|
const transform = {
|
|
...IDENTITY_TRANSFORM,
|
|
rotation: 90,
|
|
offset: { x: 10, y: 10 },
|
|
};
|
|
|
|
const cells = transformShape(shape, transform);
|
|
expect(cells).toEqual([
|
|
{ x: 10, y: 10 },
|
|
{ x: 11, y: 10 },
|
|
]);
|
|
});
|
|
});
|
|
});
|