test: expand shape-utils coverage and refactor style

This commit is contained in:
hypercross 2026-04-21 23:01:26 +08:00
parent 97ff61985a
commit 093738cd42
1 changed files with 337 additions and 325 deletions

View File

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