diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c23749..3d375b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.1.27](https://github.com/rdarida/gameforge/compare/v0.1.26...v0.1.27) (2025-09-27) + + +### Features + +* add base classes and some tests ([7feafd8](https://github.com/rdarida/gameforge/commit/7feafd8d05f5db63feb08ca94129ee46c49cb7dc)) + ### [0.1.26](https://github.com/rdarida/gameforge/compare/v0.1.25...v0.1.26) (2025-09-27) diff --git a/package-lock.json b/package-lock.json index 25fb0d0..ef352aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "license": "MIT", "dependencies": { "@pixi/sound": ">=5.2.3 <6.0.0", diff --git a/package.json b/package.json index 6d2fd06..211915f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "description": "Lightweight HTML5 boilerplate for quick 2D game prototyping", "keywords": [ "lightweight", diff --git a/src/utils/index.ts b/src/utils/index.ts index 3a2315d..6d78a00 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +export * from './list'; + export * as ColorUtil from './ColorUtil'; export * as MathUtil from './MathUtil'; export * as PointUtil from './PointUtil'; diff --git a/src/utils/list/Binder.ts b/src/utils/list/Binder.ts new file mode 100644 index 0000000..2c62b10 --- /dev/null +++ b/src/utils/list/Binder.ts @@ -0,0 +1,60 @@ +export class Binder { + protected _prev: Binder; + protected _next: Binder; + + /** + * Gets the previous Binder in the chain. + */ + public get prev(): Binder { + return this._prev; + } + + /** + * Gets the next Binder in the chain. + */ + public get next(): Binder { + return this._next; + } + + /** + * Creates a new Binder instance. + * + * Each Binder stats as a self-referencing circular node + * (`prev = this`, `next = this`); + */ + constructor() { + this._prev = this; + this._next = this; + } + + /** + * Binds the given Binder right after this one in the chain. + * + * @param next The Binder to insert after the current one. + */ + public bind(next: Binder): void { + const a = this; + const b = next; + const c = this._next; + + a._next = b; + b._prev = a; + + if (a === c) return; + + b._next = c; + c._prev = b; + } + + /** + * Unbinds this Binder instance from the chain. + */ + public unbind(): void { + const { _prev: a, _next: b } = this; + + a._next = b; + b._prev = a; + this._prev = this; + this._next = this; + } +} diff --git a/src/utils/list/Item.ts b/src/utils/list/Item.ts new file mode 100644 index 0000000..f678815 --- /dev/null +++ b/src/utils/list/Item.ts @@ -0,0 +1,36 @@ +import { Binder } from './Binder'; + +export class Item extends Binder { + protected _data: T; + + /** + * Gets the data stored in this item. + */ + public get data(): T { + return this._data; + } + + /** + * Creates a new Item instance with the given data. + * + * @param data The data to store in this item. + */ + constructor(data: T) { + super(); + + this._data = data; + } + + /** + * Converts a value or an Item into an Item instance. + * + * If the given value is already an Item, it is returned unchanged. + * Otherwise, a new Item is created to wrap the value. + * + * @param value Either a raw value of type K or an Item. + * @returns An Item instance. + */ + public static parse(value: K | Item): Item { + return value instanceof Item ? value : new Item(value); + } +} diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts new file mode 100644 index 0000000..0e17523 --- /dev/null +++ b/src/utils/list/List.ts @@ -0,0 +1,192 @@ +import { Binder } from './Binder'; +import { Item } from './Item'; + +/** + * Represends a generic doubly-linked list. + */ +export class List { + private readonly _head: Binder; + private readonly _tail: Binder; + private _length: number; + + /** + * Gets the first element in the list, + * or `undefined` if the list is empty. + */ + public get first(): Item | undefined { + return 0 < this._length ? (this._head.next as Item) : undefined; + } + + /** + * Gets the last element in the list, + * or `undefined` if the list is empty. + */ + public get last(): Item | undefined { + return 0 < this._length ? (this._tail.prev as Item) : undefined; + } + + /** + * Gets the number of elements in the list. + */ + public get length(): number { + return this._length; + } + + /** + * Creates a new empty List instance. + */ + constructor() { + this._head = new Binder(); + this._tail = new Binder(); + this._head.bind(this._tail); + + this._length = 0; + } + + /** + * Inserts a new element at the beginning of the list. + * + * @param element The element to insert. + * @returns The created Item wrapping the element. + */ + public unshift(element: T): Item { + return this.link(this._head, element); + } + + /** + * Inserts a new element to the end of the list. + * + * @param element The element to insert. + * @returns The created Item wrapping the element. + */ + public push(element: T): Item { + return this.link(this._tail.prev, element); + } + + /** + * Removes and returns the first element of the list. + * + * @returns The removed Item, or `undefined` if the list is empty. + */ + public shift(): Item | undefined { + return this.unlink(this.first); + } + + /** + * Removes and returns the last element of the list. + * + * @returns The removed Item, or `undefined` if the list is empty. + */ + public pop(): Item | undefined { + return this.unlink(this.last); + } + + /** + * Removes all elements from the list. + */ + public clear(): void { + while (0 < this.length) { + this.shift(); + } + } + + /** + * Finds the first item whose stored value mathes the given element. + * + * @param element The value to search for. + * @returns The matching Item, or `undefined` if not found. + */ + public find(element: T): Item | undefined { + for (const item of this) { + if (item.data && item.data === element) return item; + } + + return undefined; + } + + /** + * Determines whether the list contains the specified element. + * + * @param element The value to check for. + * @returns True if the element is found, otherwise false. + */ + public contains(element: T): boolean { + return this.find(element) != undefined; + } + + /** + * Performs the specified action for each element in a list. + * + * @param callbackfn A function that accepts up to three arguments. forEach calls + * the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the + * callbackfn function. If thisArg is omitted, undefined is used as the + * this value. + */ + public forEach( + callbackfn: (value: T, i: number, list: List) => void, + thisArg?: any + ): void { + let i = 0; + + for (const item of this) { + callbackfn.call(thisArg, item.data, i++, this); + } + } + + /** + * Calls a defined callback function on each element of a list, and returns + * a list that contains the results. + * + * @param callbackfn A function that accepts up to three arguments. The map + * method calls the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the + * callbackfn function. If thisArg is omitted, undefined is used as the this + * value. + */ + public map( + callbackfn: (value: T, index: number, list: List) => U, + thisArg?: any + ): List { + const list = new List(); + + let i = 0; + + for (const item of this) { + list.push(callbackfn.call(thisArg, item.data, i++, this)); + } + + return list; + } + + /** + * Returns an iterator over the list items. + */ + public *[Symbol.iterator](): IterableIterator> { + let current = this.first; + + while (current && current !== this._tail) { + yield current; + current = current.next as Item; + } + } + + private link(prev: Binder, element: T): Item { + const item = Item.parse(element); + prev.bind(item); + + this._length++; + + return item; + } + + private unlink(item: Item | undefined): Item | undefined { + if (item == undefined) return undefined; + + item.unbind(); + + this._length--; + + return item; + } +} diff --git a/src/utils/list/index.ts b/src/utils/list/index.ts new file mode 100644 index 0000000..eb62370 --- /dev/null +++ b/src/utils/list/index.ts @@ -0,0 +1,3 @@ +export * from './Binder'; +export * from './Item'; +export * from './List'; diff --git a/tests/utils/list/Binder.test.ts b/tests/utils/list/Binder.test.ts new file mode 100644 index 0000000..9f663a0 --- /dev/null +++ b/tests/utils/list/Binder.test.ts @@ -0,0 +1,56 @@ +import { Binder } from '../../../src/utils/list/Binder'; + +describe('Test Binder class', () => { + it('should be truthy', () => { + const binder = new Binder(); + expect(binder).toBeTruthy(); + expect(binder.prev).toBe(binder); + expect(binder.next).toBe(binder); + }); + + it('should bind three Binder instances together', () => { + const a = new Binder(); + const b = new Binder(); + const c = new Binder(); + + a.bind(c); + + expect(a.prev).toBe(a); + expect(a.next).toBe(c); + expect(c.prev).toBe(a); + expect(c.next).toBe(c); + + a.bind(b); + + expect(a.next).toBe(b); + expect(b.prev).toBe(a); + expect(b.next).toBe(c); + expect(c.prev).toBe(b); + }); + + it('should unlink a Binder instance from a chain', () => { + const a = new Binder(); + const b = new Binder(); + const c = new Binder(); + + a.bind(b); + b.bind(c); + + expect(a.prev).toBe(a); + expect(a.next).toBe(b); + expect(b.prev).toBe(a); + expect(b.next).toBe(c); + expect(c.prev).toBe(b); + expect(c.next).toBe(c); + + b.unbind(); + + expect(b.prev).toBe(b); + expect(b.next).toBe(b); + + expect(a.prev).toBe(a); + expect(a.next).toBe(c); + expect(c.prev).toBe(a); + expect(c.next).toBe(c); + }); +}); diff --git a/tests/utils/list/Item.test.ts b/tests/utils/list/Item.test.ts new file mode 100644 index 0000000..ddacb3a --- /dev/null +++ b/tests/utils/list/Item.test.ts @@ -0,0 +1,22 @@ +import { Item } from '../../../src/utils/list/Item'; + +describe('Test Item class', () => { + let item: Item; + + beforeEach(() => { + item = new Item('data'); + }); + + it('should be truthy', () => { + expect(item).toBeTruthy(); + expect(item.data).toBe('data'); + expect(item.prev).toBe(item); + expect(item.next).toBe(item); + }); + + it('should parse a value and return an Item instance', () => { + const item = Item.parse('valueToParse'); + expect(item.data).toBe('valueToParse'); + expect(item).toStrictEqual(Item.parse(item)); + }); +}); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts new file mode 100644 index 0000000..074283f --- /dev/null +++ b/tests/utils/list/List.test.ts @@ -0,0 +1,123 @@ +import { List } from '../../../src/utils/list/List'; +import { Item } from '../../../src/utils/list/Item'; + +describe('Test List class', () => { + let list: List; + + beforeEach(() => { + list = new List(); + }); + + it('should be truthy', () => { + expect(list).toBeTruthy(); + expect(list.length).toBe(0); + expect(list.first).toBeUndefined(); + expect(list.last).toBeUndefined(); + expect([...list].length).toBe(0); + }); + + it('should add a new element at the beginning of the list', () => { + list.unshift('firstItem1'); + expect(list.length).toBe(1); + expect(list.first?.data).toBe('firstItem1'); + + list.unshift('firstItem2'); + expect(list.length).toBe(2); + expect(list.first?.data).toBe('firstItem2'); + + expect([...list].length).toBe(2); + }); + + it('should add a new element to the end of the list', () => { + list.push('lastItem1'); + expect(list.length).toBe(1); + expect(list.last?.data).toBe('lastItem1'); + + list.push('lastItem2'); + expect(list.length).toBe(2); + expect(list.last?.data).toBe('lastItem2'); + + expect([...list].length).toBe(2); + }); + + it('should remove the first element of the list', () => { + expect(list.shift()).toBeUndefined(); + + list.unshift('firstItem1'); + const expected = list.unshift('firstItem2'); + expect(list.shift()).toBe(expected); + expect(list.length).toBe(1); + }); + + it('should remove the last element of the list', () => { + expect(list.pop()).toBeUndefined(); + + list.push('firstItem1'); + const expected = list.push('firstItem2'); + expect(list.pop()).toBe(expected); + expect(list.length).toBe(1); + }); + + it('should clear the list', () => { + for (let i = 0; i < 10; ++i) { + list.unshift(`item${i}`); + } + + expect(list.length).toBe(10); + + list.clear(); + expect(list.length).toBe(0); + }); + + it('should find the specified element', () => { + expect(list.find('toFind')).toBeUndefined(); + + const item = list.unshift('toFind'); + expect(list.find('toFind')).toBe(item); + expect(list.contains('notFound')).toBe(false); + }); + + it('should iterate over all items in order with forEach', () => { + list.push('1'); + list.push('2'); + list.push('4'); + list.push('8'); + + let counter = ''; + + list.forEach((item, i) => { + counter += item + i; + }); + + expect(counter).toBe('10214283'); + }); + + it('should iterate over all items in order with map', () => { + list.push('1'); + list.push('2'); + list.push('4'); + list.push('8'); + + const newList = list.map((item, i) => parseInt(item) + i); + + let counter = ''; + + newList.forEach(item => { + counter += item; + }); + + expect(counter).toBe('13611'); + }); + + it('sould iterate over all items in order', () => { + const elements = ['one', 'two', 'three']; + + elements.forEach(element => list.push(element)); + + const items = [...list]; + + items.forEach(({ data }, i) => { + expect(data).toBe(elements[i]); + }); + }); +}); diff --git a/tests/utils/list/index.test.ts b/tests/utils/list/index.test.ts new file mode 100644 index 0000000..2b81284 --- /dev/null +++ b/tests/utils/list/index.test.ts @@ -0,0 +1,15 @@ +import { Binder, Item, List } from '../../../src/utils/list'; + +describe('Test index.ts', () => { + it('should export list/Binder class', () => { + expect(Binder).toBeTruthy(); + }); + + it('should export list/Item class', () => { + expect(Item).toBeTruthy(); + }); + + it('should export list/List class', () => { + expect(List).toBeTruthy(); + }); +});