Compare commits

..

No commits in common. "113d240f719e33c9d2918eb7b55f06b231c65888" and "03d367c7b031aecd51d0c9e4a8b13be39f370a6a" have entirely different histories.

9 changed files with 502 additions and 501 deletions

View File

@ -3,6 +3,5 @@ export * from "./system/deck";
export * from "./system/encounter"; export * from "./system/encounter";
export * from "./system/grid-inventory"; export * from "./system/grid-inventory";
export * from "./system/map"; export * from "./system/map";
export * from "./system/utils"; export * from "./system/utils/parse-shape";
export * from "./system/types"; export * from "./system/types";
export { default as data } from "./data";

View File

@ -1,2 +1,7 @@
export * from "./factory"; export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
export * from "./types"; export {
generateDeckFromInventory,
createCard,
createPlayerDeck,
generateCardId,
} from './factory';

View File

@ -1,3 +1,24 @@
export * from "./types"; export type {
export * from "./transform"; CellCoordinate,
CellKey,
GridInventory,
InventoryItem,
MutationResult,
PlacementResult,
} from "./types";
export type { GameItemMeta, GameItem } from "./types";
export {
createGridInventory,
flipItem,
getAdjacentItems,
getItemAtCell,
getOccupiedCellSet,
moveItem,
placeItem,
removeItem,
rotateItem,
validatePlacement,
} from "./transform";
export * from "./factory"; export * from "./factory";

View File

@ -1,4 +1,3 @@
import { EffectData } from "../types";
import type { ParsedShape } from "../utils/parse-shape"; import type { ParsedShape } from "../utils/parse-shape";
import type { Transform2D } from "../utils/shape-collision"; import type { Transform2D } from "../utils/shape-collision";
import { import {
@ -11,7 +10,6 @@ import {
} from "../utils/shape-collision"; } from "../utils/shape-collision";
import type { import type {
CellKey, CellKey,
GameItemMeta,
GridInventory, GridInventory,
InventoryItem, InventoryItem,
MutationResult, MutationResult,
@ -257,21 +255,3 @@ export function getAdjacentItems<TMeta>(
return adjacent; return adjacent;
} }
// export type EffectTable = Record<string, { data: EffectData; stacks: number }>;
export function getItemEffects(inv: GridInventory<GameItemMeta>) {
const effects = {} as Record<
string,
Record<string, { data: EffectData; stacks: number }>
>;
for (const item of inv.items.values()) {
if (!item.meta) continue;
const { startEffects } = item.meta;
if (!startEffects) continue;
effects[item.id] = startEffects;
}
return effects;
}

View File

@ -1,4 +1,4 @@
import { EffectData, ItemData } from "../types"; import { ItemData } from "../types";
import type { ParsedShape } from "../utils/parse-shape"; import type { ParsedShape } from "../utils/parse-shape";
import type { Transform2D } from "../utils/shape-collision"; import type { Transform2D } from "../utils/shape-collision";
@ -64,7 +64,7 @@ export interface GameItemMeta {
itemData: ItemData; itemData: ItemData;
shape: ParsedShape; shape: ParsedShape;
consumedUses?: number; consumedUses?: number;
startEffects?: Record<string, { data: EffectData; stacks: number }>; startEffects?: Record<string, number>;
tradePrice?: number; tradePrice?: number;
} }
export type GameItem = InventoryItem<GameItemMeta>; export type GameItem = InventoryItem<GameItemMeta>;

View File

@ -1,3 +1,24 @@
export * from "./generator"; export { MapNodeType, MapLayerType } from "./types";
export * from "./navigation"; export type {
export * from "./types"; MapNode,
MapLayer,
PointCrawlMap,
MapGenerationConfig,
} from "./types";
export { generatePointCrawlMap } from "./generator";
export {
getNode,
getChildren,
getParents,
hasPath,
findAllPaths,
} from "./generator";
export {
canMoveTo,
moveToNode,
getReachableChildren,
isAtEndNode,
isAtStartNode,
} from "./navigation";

View File

@ -1,2 +0,0 @@
export * from "./parse-shape";
export * from "./shape-collision";

View File

@ -1,132 +1,129 @@
import type { ParsedShape } from "./parse-shape"; import type { ParsedShape } from './parse-shape';
/** /**
* Represents a 2D point in grid coordinates. * Represents a 2D point in grid coordinates.
*/ */
export interface Point2D { export interface Point2D {
x: number; x: number;
y: number; y: number;
} }
/** /**
* 2D transformation to apply to a shape. * 2D transformation to apply to a shape.
*/ */
export interface Transform2D { export interface Transform2D {
/** Translation offset in grid units */ /** Translation offset in grid units */
offset: Point2D; offset: Point2D;
/** Rotation in degrees (0, 90, 180, 270) */ /** Rotation in degrees (0, 90, 180, 270) */
rotation: number; rotation: number;
/** Whether to flip horizontally */ /** Whether to flip horizontally */
flipX: boolean; flipX: boolean;
/** Whether to flip vertically */ /** Whether to flip vertically */
flipY: boolean; flipY: boolean;
} }
/** /**
* Default transform (identity). * Default transform (identity).
*/ */
export const IDENTITY_TRANSFORM: Transform2D = { export const IDENTITY_TRANSFORM: Transform2D = {
offset: { x: 0, y: 0 }, offset: { x: 0, y: 0 },
rotation: 0, rotation: 0,
flipX: false, flipX: false,
flipY: false, flipY: false,
}; };
/** /**
* Gets all occupied cell coordinates from a shape. * Gets all occupied cell coordinates from a shape.
*/ */
export function getOccupiedCells(shape: ParsedShape): Point2D[] { export function getOccupiedCells(shape: ParsedShape): Point2D[] {
const cells: Point2D[] = []; const cells: Point2D[] = [];
for (let y = 0; y < shape.height; y++) { for (let y = 0; y < shape.height; y++) {
for (let x = 0; x < shape.width; x++) { for (let x = 0; x < shape.width; x++) {
if (shape.grid[y]?.[x]) { if (shape.grid[y]?.[x]) {
cells.push({ x, y }); cells.push({ x, y });
} }
}
} }
} return cells;
return cells;
} }
/** /**
* Applies a 2D transformation to a point. * Applies a 2D transformation to a point.
*/ */
export function transformPoint( export function transformPoint(
point: Point2D, point: Point2D,
transform: Transform2D, transform: Transform2D,
shapeWidth: number, shapeWidth: number,
shapeHeight: number, shapeHeight: number
): Point2D { ): Point2D {
let { x, y } = point; let { x, y } = point;
// Apply flips // Apply flips
if (transform.flipX) { if (transform.flipX) {
x = shapeWidth - 1 - x; x = shapeWidth - 1 - x;
} }
if (transform.flipY) { if (transform.flipY) {
y = shapeHeight - 1 - y; y = shapeHeight - 1 - y;
} }
// Apply rotation (around origin 0,0) // Apply rotation (around origin 0,0)
const rotation = ((transform.rotation % 360) + 360) % 360; const rotation = ((transform.rotation % 360) + 360) % 360;
let rotatedX = x; let rotatedX = x;
let rotatedY = y; let rotatedY = y;
switch (rotation) { switch (rotation) {
case 90: case 90:
rotatedX = y; rotatedX = y;
rotatedY = -x; rotatedY = -x;
break; break;
case 180: case 180:
rotatedX = -x; rotatedX = -x;
rotatedY = -y; rotatedY = -y;
break; break;
case 270: case 270:
rotatedX = -y; rotatedX = -y;
rotatedY = x; rotatedY = x;
break; break;
} }
// Apply offset // Apply offset
return { return {
x: rotatedX + transform.offset.x, x: rotatedX + transform.offset.x,
y: rotatedY + transform.offset.y, y: rotatedY + transform.offset.y,
}; };
} }
/** /**
* Transforms a shape and returnss its occupied cells in world coordinates. * Transforms a shape and returnss its occupied cells in world coordinates.
*/ */
export function transformShape( export function transformShape(shape: ParsedShape, transform: Transform2D): Point2D[] {
shape: ParsedShape, const cells = getOccupiedCells(shape);
transform: Transform2D, return cells.map(cell =>
): Point2D[] { transformPoint(cell, transform, shape.width, shape.height)
const cells = getOccupiedCells(shape); );
return cells.map((cell) =>
transformPoint(cell, transform, shape.width, shape.height),
);
} }
/** /**
* Checks if two transformed shapes collide (share any occupied cell). * Checks if two transformed shapes collide (share any occupied cell).
*/ */
export function checkCollision( export function checkCollision(
shapeA: ParsedShape, shapeA: ParsedShape,
transformA: Transform2D, transformA: Transform2D,
shapeB: ParsedShape, shapeB: ParsedShape,
transformB: Transform2D, transformB: Transform2D
): boolean { ): boolean {
const cellsA = transformShape(shapeA, transformA); const cellsA = transformShape(shapeA, transformA);
const cellsB = transformShape(shapeB, transformB); const cellsB = transformShape(shapeB, transformB);
const setA = new Set(cellsA.map((c) => `${c.x},${c.y}`)); const setA = new Set(cellsA.map(c => `${c.x},${c.y}`));
for (const cell of cellsB) { for (const cell of cellsB) {
if (setA.has(`${cell.x},${cell.y}`)) { if (setA.has(`${cell.x},${cell.y}`)) {
return true; return true;
}
} }
}
return false; return false;
} }
/** /**
@ -136,19 +133,19 @@ export function checkCollision(
* @param occupiedCells Set of occupied board cells in "x,y" format * @param occupiedCells Set of occupied board cells in "x,y" format
*/ */
export function checkBoardCollision( export function checkBoardCollision(
shape: ParsedShape, shape: ParsedShape,
transform: Transform2D, transform: Transform2D,
occupiedCells: Set<string>, occupiedCells: Set<string>
): boolean { ): boolean {
const cells = transformShape(shape, transform); const cells = transformShape(shape, transform);
for (const cell of cells) { for (const cell of cells) {
if (occupiedCells.has(`${cell.x},${cell.y}`)) { if (occupiedCells.has(`${cell.x},${cell.y}`)) {
return true; return true;
}
} }
}
return false; return false;
} }
/** /**
@ -159,47 +156,42 @@ export function checkBoardCollision(
* @param boardHeight Board height * @param boardHeight Board height
*/ */
export function checkBounds( export function checkBounds(
shape: ParsedShape, shape: ParsedShape,
transform: Transform2D, transform: Transform2D,
boardWidth: number, boardWidth: number,
boardHeight: number, boardHeight: number
): boolean { ): boolean {
const cells = transformShape(shape, transform); const cells = transformShape(shape, transform);
for (const cell of cells) { for (const cell of cells) {
if ( if (cell.x < 0 || cell.x >= boardWidth || cell.y < 0 || cell.y >= boardHeight) {
cell.x < 0 || return false;
cell.x >= boardWidth || }
cell.y < 0 ||
cell.y >= boardHeight
) {
return false;
} }
}
return true; return true;
} }
/** /**
* Validates that a placement is both in bounds and collision-free. * Validates that a placement is both in bounds and collision-free.
* @returns Object with `valid` flag and optional `reason` string * @returns Object with `valid` flag and optional `reason` string
*/ */
export function validateShapePlacement( export function validatePlacement(
shape: ParsedShape, shape: ParsedShape,
transform: Transform2D, transform: Transform2D,
boardWidth: number, boardWidth: number,
boardHeight: number, boardHeight: number,
occupiedCells: Set<string>, occupiedCells: Set<string>
): { valid: true } | { valid: false; reason: string } { ): { valid: true } | { valid: false; reason: string } {
if (!checkBounds(shape, transform, boardWidth, boardHeight)) { if (!checkBounds(shape, transform, boardWidth, boardHeight)) {
return { valid: false, reason: "超出边界" }; return { valid: false, reason: '超出边界' };
} }
if (checkBoardCollision(shape, transform, occupiedCells)) { if (checkBoardCollision(shape, transform, occupiedCells)) {
return { valid: false, reason: "与已有形状重叠" }; return { valid: false, reason: '与已有形状重叠' };
} }
return { valid: true }; return { valid: true };
} }
/** /**
@ -207,32 +199,29 @@ export function validateShapePlacement(
* @param current The current transform * @param current The current transform
* @param degrees Degrees to rotate (typically 90, 180, or 270) * @param degrees Degrees to rotate (typically 90, 180, or 270)
*/ */
export function rotateTransform( export function rotateTransform(current: Transform2D, degrees: number): Transform2D {
current: Transform2D, return {
degrees: number, ...current,
): Transform2D { rotation: ((current.rotation + degrees) % 360 + 360) % 360,
return { };
...current,
rotation: (((current.rotation + degrees) % 360) + 360) % 360,
};
} }
/** /**
* Flips a transform horizontally. * Flips a transform horizontally.
*/ */
export function flipXTransform(current: Transform2D): Transform2D { export function flipXTransform(current: Transform2D): Transform2D {
return { return {
...current, ...current,
flipX: !current.flipX, flipX: !current.flipX,
}; };
} }
/** /**
* Flips a transform vertically. * Flips a transform vertically.
*/ */
export function flipYTransform(current: Transform2D): Transform2D { export function flipYTransform(current: Transform2D): Transform2D {
return { return {
...current, ...current,
flipY: !current.flipY, flipY: !current.flipY,
}; };
} }

View File

@ -1,367 +1,355 @@
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,
validateShapePlacement, validatePlacement,
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 },
]);
});
}); });
it("should return cells for a horizontal line", () => { describe('checkCollision', () => {
const shape = parseShapeString("oe"); it('should detect collision between overlapping shapes', () => {
const cells = getOccupiedCells(shape); const shapeA = parseShapeString('o');
expect(cells).toEqual([ const shapeB = parseShapeString('o');
{ x: 0, y: 0 },
{ x: 1, y: 0 }, 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 an L shape", () => { describe('checkBoardCollision', () => {
const shape = parseShapeString("oes"); it('should detect collision with occupied cells', () => {
const cells = getOccupiedCells(shape); const shape = parseShapeString('oe');
expect(cells).toEqual([ const occupied = new Set(['0,0', '1,0']);
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
]);
});
});
describe("checkCollision", () => { const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
it("should detect collision between overlapping shapes", () => { expect(result).toBe(true);
const shapeA = parseShapeString("o"); });
const shapeB = parseShapeString("o");
const result = checkCollision( it('should not detect collision with empty board', () => {
shapeA, const shape = parseShapeString('oe');
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, const occupied = new Set<string>();
shapeB,
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
); 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);
});
}); });
it("should not detect collision between non-overlapping shapes", () => { describe('checkBounds', () => {
const shapeA = parseShapeString("o"); it('should return true for shape within bounds', () => {
const shapeB = parseShapeString("o"); const shape = parseShapeString('oe');
const result = checkCollision( const result = checkBounds(shape, IDENTITY_TRANSFORM, 10, 10);
shapeA, expect(result).toBe(true);
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, });
shapeB,
{ ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } }, it('should return false for shape outside bounds', () => {
); 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);
});
}); });
it("should detect collision with adjacent shapes", () => { describe('validatePlacement', () => {
const shapeA = parseShapeString("o"); it('should return valid for good placement', () => {
const shapeB = parseShapeString("o"); const shape = parseShapeString('oe');
const occupied = new Set<string>();
const result = checkCollision( const result = validatePlacement(shape, IDENTITY_TRANSFORM, 10, 10, occupied);
shapeA, expect(result).toEqual({ valid: true });
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, });
shapeB,
{ ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } }, it('should return invalid for out of bounds', () => {
); const shape = parseShapeString('oe');
expect(result).toBe(false); const occupied = new Set<string>();
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: '与已有形状重叠' });
});
}); });
it("should detect collision with rotation", () => { describe('transformShape', () => {
const shapeA = parseShapeString("oe"); it('should apply translation correctly', () => {
const shapeB = parseShapeString("os"); const shape = parseShapeString('o');
const transform = { ...IDENTITY_TRANSFORM, offset: { x: 5, y: 3 } };
// shapeA is horizontal at (0,0)-(1,0) const cells = transformShape(shape, transform);
// shapeB rotated 90° becomes vertical at (0,0)-(0,1) expect(cells).toEqual([{ x: 5, y: 3 }]);
// They should collide at (0,0) });
const result = checkCollision(
shapeA, it('should apply 90° rotation correctly', () => {
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }, const shape = parseShapeString('oe');
shapeB, const transform = { ...IDENTITY_TRANSFORM, rotation: 90 };
{ ...IDENTITY_TRANSFORM, rotation: 90, offset: { x: 0, y: 0 } },
); const cells = transformShape(shape, transform);
expect(result).toBe(true); 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 },
]);
});
}); });
});
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 },
]);
});
});
}); });