diff --git a/tests/samples/slay-the-spire-like/utils/shape-utils.test.ts b/tests/samples/slay-the-spire-like/utils/shape-utils.test.ts index da260d9..8eba473 100644 --- a/tests/samples/slay-the-spire-like/utils/shape-utils.test.ts +++ b/tests/samples/slay-the-spire-like/utils/shape-utils.test.ts @@ -1,355 +1,367 @@ -import { describe, it, expect } from 'vitest'; -import { parseShapeString } from '@/samples/slay-the-spire-like/system/utils/parse-shape'; +import { describe, it, expect } from "vitest"; +import { parseShapeString } from "@/samples/slay-the-spire-like/system/utils/parse-shape"; import { - checkCollision, - checkBoardCollision, - checkBounds, - validatePlacement, - transformShape, - getOccupiedCells, - IDENTITY_TRANSFORM, -} from '@/samples/slay-the-spire-like/system/utils/shape-collision'; + 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); - }); +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 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 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 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 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 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 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 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 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], - ]); - }); + 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("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 }]); }); - 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); - }); + 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 }, + ]); }); - describe('checkBoardCollision', () => { - it('should detect collision with occupied cells', () => { - const shape = parseShapeString('oe'); - const occupied = new Set(['0,0', '1,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 }, + ]); + }); + }); - const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied); - expect(result).toBe(true); - }); + describe("checkCollision", () => { + it("should detect collision between overlapping shapes", () => { + const shapeA = parseShapeString("o"); + const shapeB = parseShapeString("o"); - it('should not detect collision with empty board', () => { - const shape = parseShapeString('oe'); - const occupied = new Set(); - - 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); - }); + const result = checkCollision( + shapeA, + { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, + shapeB, + { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, + ); + expect(result).toBe(true); }); - describe('checkBounds', () => { - it('should return true for shape within bounds', () => { - const shape = parseShapeString('oe'); + it("should not detect collision between non-overlapping shapes", () => { + const shapeA = parseShapeString("o"); + const shapeB = parseShapeString("o"); - 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); - }); + const result = checkCollision( + shapeA, + { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, + shapeB, + { ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } }, + ); + expect(result).toBe(false); }); - describe('validatePlacement', () => { - it('should return valid for good placement', () => { - const shape = parseShapeString('oe'); - const occupied = new Set(); + it("should detect collision with adjacent shapes", () => { + const shapeA = parseShapeString("o"); + const shapeB = parseShapeString("o"); - const result = validatePlacement(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(); - - 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: '与已有形状重叠' }); - }); + const result = checkCollision( + shapeA, + { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, + shapeB, + { ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } }, + ); + expect(result).toBe(false); }); - describe('transformShape', () => { - it('should apply translation correctly', () => { - const shape = parseShapeString('o'); - const transform = { ...IDENTITY_TRANSFORM, offset: { x: 5, y: 3 } }; + it("should detect collision with rotation", () => { + const shapeA = parseShapeString("oe"); + const shapeB = parseShapeString("os"); - 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 }, - ]); - }); + // 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(); + + 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(); + + 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(); + + 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 }, + ]); + }); + }); });