Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './list';

export * as ColorUtil from './ColorUtil';
export * as MathUtil from './MathUtil';
export * as PointUtil from './PointUtil';
60 changes: 60 additions & 0 deletions src/utils/list/Binder.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
36 changes: 36 additions & 0 deletions src/utils/list/Item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Binder } from './Binder';

export class Item<T> 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<K>.
* @returns An Item<K> instance.
*/
public static parse<K>(value: K | Item<K>): Item<K> {
return value instanceof Item ? value : new Item(value);
}
}
192 changes: 192 additions & 0 deletions src/utils/list/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { Binder } from './Binder';
import { Item } from './Item';

/**
* Represends a generic doubly-linked list.
*/
export class List<T> {
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<T> | undefined {
return 0 < this._length ? (this._head.next as Item<T>) : undefined;
}

/**
* Gets the last element in the list,
* or `undefined` if the list is empty.
*/
public get last(): Item<T> | undefined {
return 0 < this._length ? (this._tail.prev as Item<T>) : undefined;
}

/**
* Gets the number of elements in the list.
*/
public get length(): number {
return this._length;
}

/**
* Creates a new empty List<T> 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<T> {
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<T> {
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<T> | 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<T> | 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<T> | 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<T>) => 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<U>(
callbackfn: (value: T, index: number, list: List<T>) => U,
thisArg?: any
): List<U> {
const list = new List<U>();

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<Item<T>> {
let current = this.first;

while (current && current !== this._tail) {
yield current;
current = current.next as Item<T>;
}
}

private link(prev: Binder, element: T): Item<T> {
const item = Item.parse(element);
prev.bind(item);

this._length++;

return item;
}

private unlink(item: Item<T> | undefined): Item<T> | undefined {
if (item == undefined) return undefined;

item.unbind();

this._length--;

return item;
}
}
3 changes: 3 additions & 0 deletions src/utils/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Binder';
export * from './Item';
export * from './List';
56 changes: 56 additions & 0 deletions tests/utils/list/Binder.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading