The Beginnings of a HexCells Editor

Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
3
down vote
favorite
This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.
HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.
The following controls have been implemented thus far:
- E: switch between edit and init modes
- Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode
- Right-click: cycle the number type
- Shift+right-click an empty cell: cycle which side displays a number
A live implementation can be found here.
The hex grid implementation is largely based on this excellent resource at Red Blob Games.
After poking around at various libraries, I decided to use CreateJS for the drawing.
App.ts
/* tslint:disable:no-console */
import 'lib/easeljs';
import Board from './Board';
import HexBase from './HexBase';
import OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
// Should enums get their own file?
export enum AppMode
Edit,
EditInit,
Play,
export class App
public static canvas: HTMLCanvasElement;
public static stage: createjs.Stage;
public static board: Board;
public static mode: AppMode = AppMode.Edit;
public static width(): number
return this.canvas.clientWidth * 0.9;
public static height(): number
return this.canvas.clientHeight * 0.9;
public static start(canvas: HTMLCanvasElement)
App.canvas = canvas;
App.stage = new createjs.Stage(canvas);
App.board = new Board(new Size(10, 10));
for (const key in App.board.hexes)
if (App.board.hexes.hasOwnProperty(key))
App.board.hexes[key].randomize();
// App.board.import(App.importString);
canvas.oncontextmenu = () => false;
window.addEventListener('resize', () =>
App.draw();
);
window.addEventListener('keypress', (event: KeyboardEvent) =>
if (event.key === 'e')
if (App.mode !== AppMode.Edit)
App.mode = AppMode.Edit;
else
App.mode = AppMode.EditInit;
App.board.redraw();
App.stage.update();
);
App.board.draw();
App.draw();
public static draw()
App.canvas.width = App.canvas.parentElement.offsetWidth;
App.canvas.height = App.canvas.parentElement.offsetHeight;
App.board.resize();
App.board.redraw();
App.stage.update();
Board.ts
import 'lib/easeljs';
import App from './App';
import Hex from './Hex';
import HexBase from './HexBase';
import Orientation, OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
export class Board
public background: createjs.Shape = new createjs.Shape();
public hexes: [key: string]: Hex = ;
public author: string;
public title: string;
public info: string;
public hexSize: Size;
private orientation = Orientation.flat;
private origin: Point = new Point(0, 0);
public constructor(
public hexCount: Size,
)
this.init(hexCount.width, hexCount.height);
public init(width: number, height: number)
this.hexCount = new Size(width, height);
this.hexes = ;
this.resize();
const i1 = -Math.floor(height / 2);
const i2 = i1 + height;
const j1 = -Math.floor(width / 2);
const j2 = j1 + width;
for (let j = j1; j < j2; j++)
const jOffset = -Math.floor(j / 2);
for (let i = i1 + jOffset; i < i2 + jOffset; i++)
const hex = HexBase.fromSQ(i, j);
this.hexes[hex.hash()] = Hex.fromHexBase(hex);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
hex.neighbors = ;
for (let i = 0; i < HexBase.directions.length; i++)
const n = hex.neighbor(i);
hex.neighbors.push(this.getHex(n.q, n.r));
public getHex(q: number, r: number): Hex
return this.hexes[HexBase.fromQR(q, r).hash()];
public resize()
this.hexSize = Size.square(Math.min(
Math.floor(App.width() / this.hexCount.width / 1.5),
Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
));
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
public redraw()
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
this.hexes[key].draw();
public draw()
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
App.stage.addChild(this.hexes[key].shape);
App.stage.addChild(this.hexes[key].text);
this.hexes[key].draw();
public import(s: string): boolean
const lineStrings = s.split('n');
if (lineStrings.length < 6)
return false;
const title = lineStrings[1];
const author = lineStrings[2];
let info = lineStrings[3];
if (lineStrings[4].replace(/s/, '').length > 0)
info += 'n' + lineStrings[4];
lineStrings.splice(0, 5);
for (let i = lineStrings.length - 1; i >= 0; i--)
lineStrings[i] = lineStrings[i].replace(/s/, '');
if (lineStrings[i].length === 0)
lineStrings.splice(i, 1);
if (lineStrings.length === 0)
return false;
const len = lineStrings[0].length;
if (len % 2 !== 0)
return false;
for (const lineString of lineStrings)
if (lineString.length !== len)
return false;
if (/[^oOxX\
this.init(len / 2, Math.ceil(lineStrings.length / 2));
const lines = ;
for (let k = -1; k < lineStrings.length; k += 2)
let line1 = '';
if (k > -1)
line1 = lineStrings[k];
let line2 = '';
if (k + 1 < lineStrings.length)
line2 = lineStrings[k + 1];
const cOffset = -Math.floor(len / 4);
const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
for (let i = 0; i < len; i += 2)
const lineT = i % 4 === 0 ? line2 : line1;
const st = lineT === '' ? '..' : lineT.substr(i, 2);
const h = Hex.fromAxial(i / 2 + cOffset, r);
const h2 = this.getHex(h.q, h.r);
h2.import(st);
this.draw();
App.stage.update();
public hexToPixel(hex: HexBase): Point
const o = this.orientation;
const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
return new Point(x + this.origin.x, y + this.origin.y);
public pixelToHex(point: Point): HexBase
const o = this.orientation;
const p = new Point(
(point.x - this.origin.x) / this.hexSize.width,
(point.y - this.origin.y) / this.hexSize.height);
const q = o.b0 * p.x + o.b1 * p.y;
const r = o.b2 * p.x + o.b3 * p.y;
return new HexBase(q, r, -q - r);
public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideOffset(side: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideAngle(side: number, size: Size = this.hexSize): number
const o = this.orientation;
return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
const corners: Point = ;
const center = this.hexToPixel(hex);
for (let i = 0; i < 6; i++)
const offset = this.hexCornerOffset(i, size);
corners.push(new Point(center.x + offset.x, center.y + offset.y));
return corners;
private centerOffset(): Point
const c1 = new Point(0, 0);
const c2 = new Point(0, 0);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
const corners = this.hexCorners(hex);
for (const corner of corners)
if (corner.x < c1.x) c1.x = corner.x;
if (corner.y < c1.y) c1.y = corner.y;
if (corner.x > c2.x) c2.x = corner.x;
if (corner.y > c2.y) c2.y = corner.y;
return new Point(
Math.round((c2.x - Math.abs(c1.x)) / 2),
Math.round((c2.y - Math.abs(c1.y)) / 2),
);
HexBase.ts
/* tslint:disable:no-bitwise */
import 'lib/easeljs';
import Point from './Point';
export class HexBase
public static directions: HexBase = [
new HexBase(1, 0, -1),
new HexBase(1, -1, 0),
new HexBase(0, -1, 1),
new HexBase(-1, 0, 1),
new HexBase(-1, 1, 0),
new HexBase(0, 1, -1),
];
public static fromQR(q: number, r: number): HexBase
return new HexBase(q, r, -q - r);
public static fromQS(q: number, s: number): HexBase
return new HexBase(q, -q - s, s);
public static fromRS(r: number, s: number): HexBase
return new HexBase(-r - s, r, s);
public static fromRQ(r: number, q: number): HexBase
return new HexBase(q, r, -q - r);
public static fromSQ(s: number, q: number): HexBase
return new HexBase(q, -q - s, s);
public static fromSR(s: number, r: number): HexBase
return new HexBase(-r - s, r, s);
public static fromAxial(col: number, row: number): HexBase
const q = col;
const s = row - (col - (col & 1)) / 2;
return HexBase.fromQS(q, s);
public sideCount: number = ;
public surroundCount: number = 0;
public extendCount: number = 0;
constructor(
public q: number,
public r: number,
public s: number,
)
for (let i = 0; i < 6; i++)
this.sideCount.push(0);
public toAxial(): Point
const col = this.q;
const row = this.s + (this.q - (this.q & 1)) / 2;
return new Point(col, row);
public hash(): string
return this.q + ',' + this.r;
public add(h: HexBase): HexBase
return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);
public neighbor(direction: number): HexBase
return this.add(HexBase.directions[direction]);
public round(): HexBase
let q = Math.floor(Math.round(this.q));
let r = Math.floor(Math.round(this.r));
let s = Math.floor(Math.round(this.s));
const qD = Math.abs(q - this.q);
const rD = Math.abs(r - this.r);
const sD = Math.abs(s - this.s);
if (qD > rD && qD > sD)
q = -r - s;
else
if (rD > sD)
r = -q - s;
else
s = -q - r;
return new HexBase(q, r, s);
Hex.ts
import App, AppMode from './App';
import HexBase from './HexBase';
import Point from './Point';
import Settings from './Settings';
export enum HexType
Invisible,
Normal,
Blue,
export enum HexCountType
Plain,
Invisible,
Extended,
export class Hex extends HexBase
public static fromHexBase(h: HexBase): Hex
return new Hex(h.q, h.r, h.s);
public type: HexType = HexType.Invisible;
public normalCountType: HexCountType = HexCountType.Plain;
public blueCountType: HexCountType = HexCountType.Invisible;
public invisibleCountType: HexCountType = HexCountType.Plain;
public shape: createjs.Shape = new createjs.Shape();
public text: createjs.Text = new createjs.Text();
public covered: boolean = false;
public sideCountDirection: number = -1;
public neighbors: Hex = ;
constructor(
public q: number,
public r: number,
public s: number,
)
super(q, r, s);
this.addListeners();
public updateCounts(amount: number)
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
n.surroundCount += amount;
n.extendCount += amount;
if (n.neighbors[i] !== undefined)
n.neighbors[i].extendCount += amount;
let j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].extendCount += amount;
j = (i - 3 + 6) % 6;
while (n !== undefined)
n.sideCount[j] += amount;
n = n.neighbors[i];
public cycleType()
let newType = HexType.Normal;
switch (this.type)
case HexType.Normal: newType = HexType.Blue; break;
case HexType.Blue: newType = HexType.Invisible; break;
this.changeType(newType);
this.drawAllNeighbors();
this.draw();
App.stage.update();
public cycleSideCountSide()
if (this.type !== HexType.Invisible)
return;
let i = this.sideCountDirection;
i = i === -1 ? 5 : i - 1;
while (i !== -1 && this.neighbors[i] === undefined)
i = i === -1 ? 5 : i - 1;
this.sideCountDirection = i;
if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
this.invisibleCountType = HexCountType.Plain;
this.draw();
App.stage.update();
public cycleCountType()
switch (this.type)
case HexType.Normal:
switch (this.normalCountType)
case HexCountType.Invisible:
this.normalCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.surroundCount > 1)
this.normalCountType = HexCountType.Extended;
else
this.normalCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.normalCountType = HexCountType.Invisible;
break;
case HexType.Blue:
if (this.blueCountType === HexCountType.Plain)
this.blueCountType = HexCountType.Invisible;
else
this.blueCountType = HexCountType.Plain;
break;
case HexType.Invisible:
if (this.sideCountDirection === -1)
this.cycleSideCountSide();
else
switch (this.invisibleCountType)
case HexCountType.Invisible:
this.invisibleCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.sideCount[this.sideCountDirection] > 1)
this.invisibleCountType = HexCountType.Extended;
else
this.invisibleCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.invisibleCountType = HexCountType.Invisible;
break;
break;
this.draw();
App.stage.update();
public drawAllNeighbors()
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
if (n.neighbors[i] !== undefined)
n.neighbors[i].draw();
const j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].draw();
while (n !== undefined)
n.draw();
n = n.neighbors[i];
public draw()
const p = App.board.hexToPixel(this);
this.drawHex(p);
this.drawText(p);
public changeType(type: HexType)
const prevType = this.type;
this.type = type;
if (prevType === HexType.Blue && type !== HexType.Blue)
this.updateCounts(-1);
else if (prevType !== HexType.Blue && type === HexType.Blue)
this.updateCounts(1);
public changeCountType(type: HexCountType)
switch (this.type)
case HexType.Normal:
this.normalCountType = type;
break;
case HexType.Blue:
this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
break;
case HexType.Invisible:
this.invisibleCountType = type;
break;
public randomize()
const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
this.changeType(types[Math.floor(Math.random() * 3)]);
const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);
public import(s: string)
const typeS = s.substr(0, 1);
const countS = s.substr(1, 1);
this.covered = false;
switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;
switch (countS)
case '.': this.changeCountType(HexCountType.Invisible); break;
case '+': this.changeCountType(HexCountType.Plain); break;
case 'c': this.changeCountType(HexCountType.Extended); break;
case 'n': this.changeCountType(HexCountType.Extended); break;
private color()
let color = Settings.hexColors.normal;
if (this.covered && App.mode !== AppMode.Edit)
color = Settings.hexColors.covered;
else
switch (this.type)
case HexType.Invisible:
if (App.mode === AppMode.Edit)
color = Settings.hexColors.invisibleEditing;
else
color = Settings.hexColors.invisible;
break;
case HexType.Blue:
color = Settings.hexColors.blue;
break;
return color;
private drawHex(p: Point)
const color = this.color();
const g = this.shape.graphics;
g.clear();
const size = App.board.hexSize;
g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);
private drawText(p: Point)
this.text.x = p.x;
this.text.y = p.y;
this.text.text = '';
this.text.textBaseline = 'middle';
this.text.textAlign = 'center';
this.text.rotation = 0;
if (this.covered && App.mode !== AppMode.Edit)
return;
switch (this.type)
case HexType.Normal: this.drawTextNormal(); break;
case HexType.Blue: this.drawTextBlue(); break;
case HexType.Invisible: this.drawTextInvisible(); break;
private drawTextNormal()
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
this.text.color = Settings.hexColors.normal.text;
if (this.normalCountType === HexCountType.Invisible)
this.text.text = '?';
else
this.text.text = this.surroundCount.toString();
if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
let consecutive = true;
if (this.surroundCount < 5)
let i = 0;
for (;
this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
i++);
for (;
this.neighbors[i] === undefined
if (consecutive)
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private drawTextBlue()
if (this.blueCountType !== HexCountType.Invisible)
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
this.text.color = Settings.hexColors.blue.text;
this.text.text = this.extendCount.toString();
private drawTextInvisible()
if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
const size = App.board.hexSize;
const d = (6 - this.sideCountDirection) % 6;
const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
this.text.x += pOffset.x;
this.text.y += pOffset.y;
this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
this.text.color = Settings.hexColors.invisible.text;
this.text.text = this.sideCount[this.sideCountDirection].toString();
if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
const i = this.sideCountDirection;
let c = 0;
let n = this.neighbors[i];
while (n !== undefined && n.type !== HexType.Blue)
n = n.neighbors[i];
while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
if (n.type === HexType.Blue)
c++;
n = n.neighbors[i];
if (c === this.sideCount[i])
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private addListeners()
this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
if (event.nativeEvent.which === 1)
this.handleLeftClick(event);
else if (event.nativeEvent.which === 3)
this.handleRightClick(event);
);
private handleLeftClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
this.cycleType();
else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
this.covered = !this.covered;
this.draw();
App.stage.update();
private handleRightClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
this.cycleSideCountSide();
else
this.cycleCountType();
Orientation.ts
export enum OrientationType
Pointy,
Flat,
export class Orientation
public static pointy: Orientation = new Orientation(
Math.sqrt(3),
Math.sqrt(3) / 2,
0,
3 / 2,
Math.sqrt(3) / 3,
-1 / 3,
0,
2 / 3,
0.5,
);
public static flat: Orientation = new Orientation(
3 / 2,
0,
Math.sqrt(3) / 2,
Math.sqrt(3),
2 / 3,
0,
-1 / 3,
Math.sqrt(3) / 3,
0,
);
constructor(
public f0: number,
public f1: number,
public f2: number,
public f3: number,
public b0: number,
public b1: number,
public b2: number,
public b3: number,
public startAngle: number,
)
Point.ts
export class Point
constructor(
public x: number,
public y: number,
)
Settings.ts
// Is this a bad idea?
export class Settings
public static hexColors =
blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
invisible : border: "transparent", fill: "transparent", text: "#464646" ,
invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
;
public static hexFontScales =
blue : 0.72,
invisible : 0.64,
normal : 0.72,
;
Size.ts
export class Size
public static square(width: number)
return new Size(width, width);
constructor(
public width: number,
public height: number,
)
public scale(value: number)
return new Size(this.width * value, this.height * value);
game typescript
add a comment |Â
up vote
3
down vote
favorite
This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.
HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.
The following controls have been implemented thus far:
- E: switch between edit and init modes
- Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode
- Right-click: cycle the number type
- Shift+right-click an empty cell: cycle which side displays a number
A live implementation can be found here.
The hex grid implementation is largely based on this excellent resource at Red Blob Games.
After poking around at various libraries, I decided to use CreateJS for the drawing.
App.ts
/* tslint:disable:no-console */
import 'lib/easeljs';
import Board from './Board';
import HexBase from './HexBase';
import OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
// Should enums get their own file?
export enum AppMode
Edit,
EditInit,
Play,
export class App
public static canvas: HTMLCanvasElement;
public static stage: createjs.Stage;
public static board: Board;
public static mode: AppMode = AppMode.Edit;
public static width(): number
return this.canvas.clientWidth * 0.9;
public static height(): number
return this.canvas.clientHeight * 0.9;
public static start(canvas: HTMLCanvasElement)
App.canvas = canvas;
App.stage = new createjs.Stage(canvas);
App.board = new Board(new Size(10, 10));
for (const key in App.board.hexes)
if (App.board.hexes.hasOwnProperty(key))
App.board.hexes[key].randomize();
// App.board.import(App.importString);
canvas.oncontextmenu = () => false;
window.addEventListener('resize', () =>
App.draw();
);
window.addEventListener('keypress', (event: KeyboardEvent) =>
if (event.key === 'e')
if (App.mode !== AppMode.Edit)
App.mode = AppMode.Edit;
else
App.mode = AppMode.EditInit;
App.board.redraw();
App.stage.update();
);
App.board.draw();
App.draw();
public static draw()
App.canvas.width = App.canvas.parentElement.offsetWidth;
App.canvas.height = App.canvas.parentElement.offsetHeight;
App.board.resize();
App.board.redraw();
App.stage.update();
Board.ts
import 'lib/easeljs';
import App from './App';
import Hex from './Hex';
import HexBase from './HexBase';
import Orientation, OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
export class Board
public background: createjs.Shape = new createjs.Shape();
public hexes: [key: string]: Hex = ;
public author: string;
public title: string;
public info: string;
public hexSize: Size;
private orientation = Orientation.flat;
private origin: Point = new Point(0, 0);
public constructor(
public hexCount: Size,
)
this.init(hexCount.width, hexCount.height);
public init(width: number, height: number)
this.hexCount = new Size(width, height);
this.hexes = ;
this.resize();
const i1 = -Math.floor(height / 2);
const i2 = i1 + height;
const j1 = -Math.floor(width / 2);
const j2 = j1 + width;
for (let j = j1; j < j2; j++)
const jOffset = -Math.floor(j / 2);
for (let i = i1 + jOffset; i < i2 + jOffset; i++)
const hex = HexBase.fromSQ(i, j);
this.hexes[hex.hash()] = Hex.fromHexBase(hex);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
hex.neighbors = ;
for (let i = 0; i < HexBase.directions.length; i++)
const n = hex.neighbor(i);
hex.neighbors.push(this.getHex(n.q, n.r));
public getHex(q: number, r: number): Hex
return this.hexes[HexBase.fromQR(q, r).hash()];
public resize()
this.hexSize = Size.square(Math.min(
Math.floor(App.width() / this.hexCount.width / 1.5),
Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
));
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
public redraw()
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
this.hexes[key].draw();
public draw()
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
App.stage.addChild(this.hexes[key].shape);
App.stage.addChild(this.hexes[key].text);
this.hexes[key].draw();
public import(s: string): boolean
const lineStrings = s.split('n');
if (lineStrings.length < 6)
return false;
const title = lineStrings[1];
const author = lineStrings[2];
let info = lineStrings[3];
if (lineStrings[4].replace(/s/, '').length > 0)
info += 'n' + lineStrings[4];
lineStrings.splice(0, 5);
for (let i = lineStrings.length - 1; i >= 0; i--)
lineStrings[i] = lineStrings[i].replace(/s/, '');
if (lineStrings[i].length === 0)
lineStrings.splice(i, 1);
if (lineStrings.length === 0)
return false;
const len = lineStrings[0].length;
if (len % 2 !== 0)
return false;
for (const lineString of lineStrings)
if (lineString.length !== len)
return false;
if (/[^oOxX\
this.init(len / 2, Math.ceil(lineStrings.length / 2));
const lines = ;
for (let k = -1; k < lineStrings.length; k += 2)
let line1 = '';
if (k > -1)
line1 = lineStrings[k];
let line2 = '';
if (k + 1 < lineStrings.length)
line2 = lineStrings[k + 1];
const cOffset = -Math.floor(len / 4);
const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
for (let i = 0; i < len; i += 2)
const lineT = i % 4 === 0 ? line2 : line1;
const st = lineT === '' ? '..' : lineT.substr(i, 2);
const h = Hex.fromAxial(i / 2 + cOffset, r);
const h2 = this.getHex(h.q, h.r);
h2.import(st);
this.draw();
App.stage.update();
public hexToPixel(hex: HexBase): Point
const o = this.orientation;
const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
return new Point(x + this.origin.x, y + this.origin.y);
public pixelToHex(point: Point): HexBase
const o = this.orientation;
const p = new Point(
(point.x - this.origin.x) / this.hexSize.width,
(point.y - this.origin.y) / this.hexSize.height);
const q = o.b0 * p.x + o.b1 * p.y;
const r = o.b2 * p.x + o.b3 * p.y;
return new HexBase(q, r, -q - r);
public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideOffset(side: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideAngle(side: number, size: Size = this.hexSize): number
const o = this.orientation;
return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
const corners: Point = ;
const center = this.hexToPixel(hex);
for (let i = 0; i < 6; i++)
const offset = this.hexCornerOffset(i, size);
corners.push(new Point(center.x + offset.x, center.y + offset.y));
return corners;
private centerOffset(): Point
const c1 = new Point(0, 0);
const c2 = new Point(0, 0);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
const corners = this.hexCorners(hex);
for (const corner of corners)
if (corner.x < c1.x) c1.x = corner.x;
if (corner.y < c1.y) c1.y = corner.y;
if (corner.x > c2.x) c2.x = corner.x;
if (corner.y > c2.y) c2.y = corner.y;
return new Point(
Math.round((c2.x - Math.abs(c1.x)) / 2),
Math.round((c2.y - Math.abs(c1.y)) / 2),
);
HexBase.ts
/* tslint:disable:no-bitwise */
import 'lib/easeljs';
import Point from './Point';
export class HexBase
public static directions: HexBase = [
new HexBase(1, 0, -1),
new HexBase(1, -1, 0),
new HexBase(0, -1, 1),
new HexBase(-1, 0, 1),
new HexBase(-1, 1, 0),
new HexBase(0, 1, -1),
];
public static fromQR(q: number, r: number): HexBase
return new HexBase(q, r, -q - r);
public static fromQS(q: number, s: number): HexBase
return new HexBase(q, -q - s, s);
public static fromRS(r: number, s: number): HexBase
return new HexBase(-r - s, r, s);
public static fromRQ(r: number, q: number): HexBase
return new HexBase(q, r, -q - r);
public static fromSQ(s: number, q: number): HexBase
return new HexBase(q, -q - s, s);
public static fromSR(s: number, r: number): HexBase
return new HexBase(-r - s, r, s);
public static fromAxial(col: number, row: number): HexBase
const q = col;
const s = row - (col - (col & 1)) / 2;
return HexBase.fromQS(q, s);
public sideCount: number = ;
public surroundCount: number = 0;
public extendCount: number = 0;
constructor(
public q: number,
public r: number,
public s: number,
)
for (let i = 0; i < 6; i++)
this.sideCount.push(0);
public toAxial(): Point
const col = this.q;
const row = this.s + (this.q - (this.q & 1)) / 2;
return new Point(col, row);
public hash(): string
return this.q + ',' + this.r;
public add(h: HexBase): HexBase
return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);
public neighbor(direction: number): HexBase
return this.add(HexBase.directions[direction]);
public round(): HexBase
let q = Math.floor(Math.round(this.q));
let r = Math.floor(Math.round(this.r));
let s = Math.floor(Math.round(this.s));
const qD = Math.abs(q - this.q);
const rD = Math.abs(r - this.r);
const sD = Math.abs(s - this.s);
if (qD > rD && qD > sD)
q = -r - s;
else
if (rD > sD)
r = -q - s;
else
s = -q - r;
return new HexBase(q, r, s);
Hex.ts
import App, AppMode from './App';
import HexBase from './HexBase';
import Point from './Point';
import Settings from './Settings';
export enum HexType
Invisible,
Normal,
Blue,
export enum HexCountType
Plain,
Invisible,
Extended,
export class Hex extends HexBase
public static fromHexBase(h: HexBase): Hex
return new Hex(h.q, h.r, h.s);
public type: HexType = HexType.Invisible;
public normalCountType: HexCountType = HexCountType.Plain;
public blueCountType: HexCountType = HexCountType.Invisible;
public invisibleCountType: HexCountType = HexCountType.Plain;
public shape: createjs.Shape = new createjs.Shape();
public text: createjs.Text = new createjs.Text();
public covered: boolean = false;
public sideCountDirection: number = -1;
public neighbors: Hex = ;
constructor(
public q: number,
public r: number,
public s: number,
)
super(q, r, s);
this.addListeners();
public updateCounts(amount: number)
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
n.surroundCount += amount;
n.extendCount += amount;
if (n.neighbors[i] !== undefined)
n.neighbors[i].extendCount += amount;
let j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].extendCount += amount;
j = (i - 3 + 6) % 6;
while (n !== undefined)
n.sideCount[j] += amount;
n = n.neighbors[i];
public cycleType()
let newType = HexType.Normal;
switch (this.type)
case HexType.Normal: newType = HexType.Blue; break;
case HexType.Blue: newType = HexType.Invisible; break;
this.changeType(newType);
this.drawAllNeighbors();
this.draw();
App.stage.update();
public cycleSideCountSide()
if (this.type !== HexType.Invisible)
return;
let i = this.sideCountDirection;
i = i === -1 ? 5 : i - 1;
while (i !== -1 && this.neighbors[i] === undefined)
i = i === -1 ? 5 : i - 1;
this.sideCountDirection = i;
if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
this.invisibleCountType = HexCountType.Plain;
this.draw();
App.stage.update();
public cycleCountType()
switch (this.type)
case HexType.Normal:
switch (this.normalCountType)
case HexCountType.Invisible:
this.normalCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.surroundCount > 1)
this.normalCountType = HexCountType.Extended;
else
this.normalCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.normalCountType = HexCountType.Invisible;
break;
case HexType.Blue:
if (this.blueCountType === HexCountType.Plain)
this.blueCountType = HexCountType.Invisible;
else
this.blueCountType = HexCountType.Plain;
break;
case HexType.Invisible:
if (this.sideCountDirection === -1)
this.cycleSideCountSide();
else
switch (this.invisibleCountType)
case HexCountType.Invisible:
this.invisibleCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.sideCount[this.sideCountDirection] > 1)
this.invisibleCountType = HexCountType.Extended;
else
this.invisibleCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.invisibleCountType = HexCountType.Invisible;
break;
break;
this.draw();
App.stage.update();
public drawAllNeighbors()
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
if (n.neighbors[i] !== undefined)
n.neighbors[i].draw();
const j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].draw();
while (n !== undefined)
n.draw();
n = n.neighbors[i];
public draw()
const p = App.board.hexToPixel(this);
this.drawHex(p);
this.drawText(p);
public changeType(type: HexType)
const prevType = this.type;
this.type = type;
if (prevType === HexType.Blue && type !== HexType.Blue)
this.updateCounts(-1);
else if (prevType !== HexType.Blue && type === HexType.Blue)
this.updateCounts(1);
public changeCountType(type: HexCountType)
switch (this.type)
case HexType.Normal:
this.normalCountType = type;
break;
case HexType.Blue:
this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
break;
case HexType.Invisible:
this.invisibleCountType = type;
break;
public randomize()
const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
this.changeType(types[Math.floor(Math.random() * 3)]);
const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);
public import(s: string)
const typeS = s.substr(0, 1);
const countS = s.substr(1, 1);
this.covered = false;
switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;
switch (countS)
case '.': this.changeCountType(HexCountType.Invisible); break;
case '+': this.changeCountType(HexCountType.Plain); break;
case 'c': this.changeCountType(HexCountType.Extended); break;
case 'n': this.changeCountType(HexCountType.Extended); break;
private color()
let color = Settings.hexColors.normal;
if (this.covered && App.mode !== AppMode.Edit)
color = Settings.hexColors.covered;
else
switch (this.type)
case HexType.Invisible:
if (App.mode === AppMode.Edit)
color = Settings.hexColors.invisibleEditing;
else
color = Settings.hexColors.invisible;
break;
case HexType.Blue:
color = Settings.hexColors.blue;
break;
return color;
private drawHex(p: Point)
const color = this.color();
const g = this.shape.graphics;
g.clear();
const size = App.board.hexSize;
g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);
private drawText(p: Point)
this.text.x = p.x;
this.text.y = p.y;
this.text.text = '';
this.text.textBaseline = 'middle';
this.text.textAlign = 'center';
this.text.rotation = 0;
if (this.covered && App.mode !== AppMode.Edit)
return;
switch (this.type)
case HexType.Normal: this.drawTextNormal(); break;
case HexType.Blue: this.drawTextBlue(); break;
case HexType.Invisible: this.drawTextInvisible(); break;
private drawTextNormal()
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
this.text.color = Settings.hexColors.normal.text;
if (this.normalCountType === HexCountType.Invisible)
this.text.text = '?';
else
this.text.text = this.surroundCount.toString();
if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
let consecutive = true;
if (this.surroundCount < 5)
let i = 0;
for (;
this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
i++);
for (;
this.neighbors[i] === undefined
if (consecutive)
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private drawTextBlue()
if (this.blueCountType !== HexCountType.Invisible)
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
this.text.color = Settings.hexColors.blue.text;
this.text.text = this.extendCount.toString();
private drawTextInvisible()
if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
const size = App.board.hexSize;
const d = (6 - this.sideCountDirection) % 6;
const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
this.text.x += pOffset.x;
this.text.y += pOffset.y;
this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
this.text.color = Settings.hexColors.invisible.text;
this.text.text = this.sideCount[this.sideCountDirection].toString();
if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
const i = this.sideCountDirection;
let c = 0;
let n = this.neighbors[i];
while (n !== undefined && n.type !== HexType.Blue)
n = n.neighbors[i];
while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
if (n.type === HexType.Blue)
c++;
n = n.neighbors[i];
if (c === this.sideCount[i])
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private addListeners()
this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
if (event.nativeEvent.which === 1)
this.handleLeftClick(event);
else if (event.nativeEvent.which === 3)
this.handleRightClick(event);
);
private handleLeftClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
this.cycleType();
else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
this.covered = !this.covered;
this.draw();
App.stage.update();
private handleRightClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
this.cycleSideCountSide();
else
this.cycleCountType();
Orientation.ts
export enum OrientationType
Pointy,
Flat,
export class Orientation
public static pointy: Orientation = new Orientation(
Math.sqrt(3),
Math.sqrt(3) / 2,
0,
3 / 2,
Math.sqrt(3) / 3,
-1 / 3,
0,
2 / 3,
0.5,
);
public static flat: Orientation = new Orientation(
3 / 2,
0,
Math.sqrt(3) / 2,
Math.sqrt(3),
2 / 3,
0,
-1 / 3,
Math.sqrt(3) / 3,
0,
);
constructor(
public f0: number,
public f1: number,
public f2: number,
public f3: number,
public b0: number,
public b1: number,
public b2: number,
public b3: number,
public startAngle: number,
)
Point.ts
export class Point
constructor(
public x: number,
public y: number,
)
Settings.ts
// Is this a bad idea?
export class Settings
public static hexColors =
blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
invisible : border: "transparent", fill: "transparent", text: "#464646" ,
invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
;
public static hexFontScales =
blue : 0.72,
invisible : 0.64,
normal : 0.72,
;
Size.ts
export class Size
public static square(width: number)
return new Size(width, width);
constructor(
public width: number,
public height: number,
)
public scale(value: number)
return new Size(this.width * value, this.height * value);
game typescript
add a comment |Â
up vote
3
down vote
favorite
up vote
3
down vote
favorite
This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.
HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.
The following controls have been implemented thus far:
- E: switch between edit and init modes
- Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode
- Right-click: cycle the number type
- Shift+right-click an empty cell: cycle which side displays a number
A live implementation can be found here.
The hex grid implementation is largely based on this excellent resource at Red Blob Games.
After poking around at various libraries, I decided to use CreateJS for the drawing.
App.ts
/* tslint:disable:no-console */
import 'lib/easeljs';
import Board from './Board';
import HexBase from './HexBase';
import OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
// Should enums get their own file?
export enum AppMode
Edit,
EditInit,
Play,
export class App
public static canvas: HTMLCanvasElement;
public static stage: createjs.Stage;
public static board: Board;
public static mode: AppMode = AppMode.Edit;
public static width(): number
return this.canvas.clientWidth * 0.9;
public static height(): number
return this.canvas.clientHeight * 0.9;
public static start(canvas: HTMLCanvasElement)
App.canvas = canvas;
App.stage = new createjs.Stage(canvas);
App.board = new Board(new Size(10, 10));
for (const key in App.board.hexes)
if (App.board.hexes.hasOwnProperty(key))
App.board.hexes[key].randomize();
// App.board.import(App.importString);
canvas.oncontextmenu = () => false;
window.addEventListener('resize', () =>
App.draw();
);
window.addEventListener('keypress', (event: KeyboardEvent) =>
if (event.key === 'e')
if (App.mode !== AppMode.Edit)
App.mode = AppMode.Edit;
else
App.mode = AppMode.EditInit;
App.board.redraw();
App.stage.update();
);
App.board.draw();
App.draw();
public static draw()
App.canvas.width = App.canvas.parentElement.offsetWidth;
App.canvas.height = App.canvas.parentElement.offsetHeight;
App.board.resize();
App.board.redraw();
App.stage.update();
Board.ts
import 'lib/easeljs';
import App from './App';
import Hex from './Hex';
import HexBase from './HexBase';
import Orientation, OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
export class Board
public background: createjs.Shape = new createjs.Shape();
public hexes: [key: string]: Hex = ;
public author: string;
public title: string;
public info: string;
public hexSize: Size;
private orientation = Orientation.flat;
private origin: Point = new Point(0, 0);
public constructor(
public hexCount: Size,
)
this.init(hexCount.width, hexCount.height);
public init(width: number, height: number)
this.hexCount = new Size(width, height);
this.hexes = ;
this.resize();
const i1 = -Math.floor(height / 2);
const i2 = i1 + height;
const j1 = -Math.floor(width / 2);
const j2 = j1 + width;
for (let j = j1; j < j2; j++)
const jOffset = -Math.floor(j / 2);
for (let i = i1 + jOffset; i < i2 + jOffset; i++)
const hex = HexBase.fromSQ(i, j);
this.hexes[hex.hash()] = Hex.fromHexBase(hex);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
hex.neighbors = ;
for (let i = 0; i < HexBase.directions.length; i++)
const n = hex.neighbor(i);
hex.neighbors.push(this.getHex(n.q, n.r));
public getHex(q: number, r: number): Hex
return this.hexes[HexBase.fromQR(q, r).hash()];
public resize()
this.hexSize = Size.square(Math.min(
Math.floor(App.width() / this.hexCount.width / 1.5),
Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
));
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
public redraw()
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
this.hexes[key].draw();
public draw()
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
App.stage.addChild(this.hexes[key].shape);
App.stage.addChild(this.hexes[key].text);
this.hexes[key].draw();
public import(s: string): boolean
const lineStrings = s.split('n');
if (lineStrings.length < 6)
return false;
const title = lineStrings[1];
const author = lineStrings[2];
let info = lineStrings[3];
if (lineStrings[4].replace(/s/, '').length > 0)
info += 'n' + lineStrings[4];
lineStrings.splice(0, 5);
for (let i = lineStrings.length - 1; i >= 0; i--)
lineStrings[i] = lineStrings[i].replace(/s/, '');
if (lineStrings[i].length === 0)
lineStrings.splice(i, 1);
if (lineStrings.length === 0)
return false;
const len = lineStrings[0].length;
if (len % 2 !== 0)
return false;
for (const lineString of lineStrings)
if (lineString.length !== len)
return false;
if (/[^oOxX\
this.init(len / 2, Math.ceil(lineStrings.length / 2));
const lines = ;
for (let k = -1; k < lineStrings.length; k += 2)
let line1 = '';
if (k > -1)
line1 = lineStrings[k];
let line2 = '';
if (k + 1 < lineStrings.length)
line2 = lineStrings[k + 1];
const cOffset = -Math.floor(len / 4);
const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
for (let i = 0; i < len; i += 2)
const lineT = i % 4 === 0 ? line2 : line1;
const st = lineT === '' ? '..' : lineT.substr(i, 2);
const h = Hex.fromAxial(i / 2 + cOffset, r);
const h2 = this.getHex(h.q, h.r);
h2.import(st);
this.draw();
App.stage.update();
public hexToPixel(hex: HexBase): Point
const o = this.orientation;
const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
return new Point(x + this.origin.x, y + this.origin.y);
public pixelToHex(point: Point): HexBase
const o = this.orientation;
const p = new Point(
(point.x - this.origin.x) / this.hexSize.width,
(point.y - this.origin.y) / this.hexSize.height);
const q = o.b0 * p.x + o.b1 * p.y;
const r = o.b2 * p.x + o.b3 * p.y;
return new HexBase(q, r, -q - r);
public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideOffset(side: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideAngle(side: number, size: Size = this.hexSize): number
const o = this.orientation;
return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
const corners: Point = ;
const center = this.hexToPixel(hex);
for (let i = 0; i < 6; i++)
const offset = this.hexCornerOffset(i, size);
corners.push(new Point(center.x + offset.x, center.y + offset.y));
return corners;
private centerOffset(): Point
const c1 = new Point(0, 0);
const c2 = new Point(0, 0);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
const corners = this.hexCorners(hex);
for (const corner of corners)
if (corner.x < c1.x) c1.x = corner.x;
if (corner.y < c1.y) c1.y = corner.y;
if (corner.x > c2.x) c2.x = corner.x;
if (corner.y > c2.y) c2.y = corner.y;
return new Point(
Math.round((c2.x - Math.abs(c1.x)) / 2),
Math.round((c2.y - Math.abs(c1.y)) / 2),
);
HexBase.ts
/* tslint:disable:no-bitwise */
import 'lib/easeljs';
import Point from './Point';
export class HexBase
public static directions: HexBase = [
new HexBase(1, 0, -1),
new HexBase(1, -1, 0),
new HexBase(0, -1, 1),
new HexBase(-1, 0, 1),
new HexBase(-1, 1, 0),
new HexBase(0, 1, -1),
];
public static fromQR(q: number, r: number): HexBase
return new HexBase(q, r, -q - r);
public static fromQS(q: number, s: number): HexBase
return new HexBase(q, -q - s, s);
public static fromRS(r: number, s: number): HexBase
return new HexBase(-r - s, r, s);
public static fromRQ(r: number, q: number): HexBase
return new HexBase(q, r, -q - r);
public static fromSQ(s: number, q: number): HexBase
return new HexBase(q, -q - s, s);
public static fromSR(s: number, r: number): HexBase
return new HexBase(-r - s, r, s);
public static fromAxial(col: number, row: number): HexBase
const q = col;
const s = row - (col - (col & 1)) / 2;
return HexBase.fromQS(q, s);
public sideCount: number = ;
public surroundCount: number = 0;
public extendCount: number = 0;
constructor(
public q: number,
public r: number,
public s: number,
)
for (let i = 0; i < 6; i++)
this.sideCount.push(0);
public toAxial(): Point
const col = this.q;
const row = this.s + (this.q - (this.q & 1)) / 2;
return new Point(col, row);
public hash(): string
return this.q + ',' + this.r;
public add(h: HexBase): HexBase
return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);
public neighbor(direction: number): HexBase
return this.add(HexBase.directions[direction]);
public round(): HexBase
let q = Math.floor(Math.round(this.q));
let r = Math.floor(Math.round(this.r));
let s = Math.floor(Math.round(this.s));
const qD = Math.abs(q - this.q);
const rD = Math.abs(r - this.r);
const sD = Math.abs(s - this.s);
if (qD > rD && qD > sD)
q = -r - s;
else
if (rD > sD)
r = -q - s;
else
s = -q - r;
return new HexBase(q, r, s);
Hex.ts
import App, AppMode from './App';
import HexBase from './HexBase';
import Point from './Point';
import Settings from './Settings';
export enum HexType
Invisible,
Normal,
Blue,
export enum HexCountType
Plain,
Invisible,
Extended,
export class Hex extends HexBase
public static fromHexBase(h: HexBase): Hex
return new Hex(h.q, h.r, h.s);
public type: HexType = HexType.Invisible;
public normalCountType: HexCountType = HexCountType.Plain;
public blueCountType: HexCountType = HexCountType.Invisible;
public invisibleCountType: HexCountType = HexCountType.Plain;
public shape: createjs.Shape = new createjs.Shape();
public text: createjs.Text = new createjs.Text();
public covered: boolean = false;
public sideCountDirection: number = -1;
public neighbors: Hex = ;
constructor(
public q: number,
public r: number,
public s: number,
)
super(q, r, s);
this.addListeners();
public updateCounts(amount: number)
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
n.surroundCount += amount;
n.extendCount += amount;
if (n.neighbors[i] !== undefined)
n.neighbors[i].extendCount += amount;
let j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].extendCount += amount;
j = (i - 3 + 6) % 6;
while (n !== undefined)
n.sideCount[j] += amount;
n = n.neighbors[i];
public cycleType()
let newType = HexType.Normal;
switch (this.type)
case HexType.Normal: newType = HexType.Blue; break;
case HexType.Blue: newType = HexType.Invisible; break;
this.changeType(newType);
this.drawAllNeighbors();
this.draw();
App.stage.update();
public cycleSideCountSide()
if (this.type !== HexType.Invisible)
return;
let i = this.sideCountDirection;
i = i === -1 ? 5 : i - 1;
while (i !== -1 && this.neighbors[i] === undefined)
i = i === -1 ? 5 : i - 1;
this.sideCountDirection = i;
if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
this.invisibleCountType = HexCountType.Plain;
this.draw();
App.stage.update();
public cycleCountType()
switch (this.type)
case HexType.Normal:
switch (this.normalCountType)
case HexCountType.Invisible:
this.normalCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.surroundCount > 1)
this.normalCountType = HexCountType.Extended;
else
this.normalCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.normalCountType = HexCountType.Invisible;
break;
case HexType.Blue:
if (this.blueCountType === HexCountType.Plain)
this.blueCountType = HexCountType.Invisible;
else
this.blueCountType = HexCountType.Plain;
break;
case HexType.Invisible:
if (this.sideCountDirection === -1)
this.cycleSideCountSide();
else
switch (this.invisibleCountType)
case HexCountType.Invisible:
this.invisibleCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.sideCount[this.sideCountDirection] > 1)
this.invisibleCountType = HexCountType.Extended;
else
this.invisibleCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.invisibleCountType = HexCountType.Invisible;
break;
break;
this.draw();
App.stage.update();
public drawAllNeighbors()
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
if (n.neighbors[i] !== undefined)
n.neighbors[i].draw();
const j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].draw();
while (n !== undefined)
n.draw();
n = n.neighbors[i];
public draw()
const p = App.board.hexToPixel(this);
this.drawHex(p);
this.drawText(p);
public changeType(type: HexType)
const prevType = this.type;
this.type = type;
if (prevType === HexType.Blue && type !== HexType.Blue)
this.updateCounts(-1);
else if (prevType !== HexType.Blue && type === HexType.Blue)
this.updateCounts(1);
public changeCountType(type: HexCountType)
switch (this.type)
case HexType.Normal:
this.normalCountType = type;
break;
case HexType.Blue:
this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
break;
case HexType.Invisible:
this.invisibleCountType = type;
break;
public randomize()
const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
this.changeType(types[Math.floor(Math.random() * 3)]);
const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);
public import(s: string)
const typeS = s.substr(0, 1);
const countS = s.substr(1, 1);
this.covered = false;
switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;
switch (countS)
case '.': this.changeCountType(HexCountType.Invisible); break;
case '+': this.changeCountType(HexCountType.Plain); break;
case 'c': this.changeCountType(HexCountType.Extended); break;
case 'n': this.changeCountType(HexCountType.Extended); break;
private color()
let color = Settings.hexColors.normal;
if (this.covered && App.mode !== AppMode.Edit)
color = Settings.hexColors.covered;
else
switch (this.type)
case HexType.Invisible:
if (App.mode === AppMode.Edit)
color = Settings.hexColors.invisibleEditing;
else
color = Settings.hexColors.invisible;
break;
case HexType.Blue:
color = Settings.hexColors.blue;
break;
return color;
private drawHex(p: Point)
const color = this.color();
const g = this.shape.graphics;
g.clear();
const size = App.board.hexSize;
g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);
private drawText(p: Point)
this.text.x = p.x;
this.text.y = p.y;
this.text.text = '';
this.text.textBaseline = 'middle';
this.text.textAlign = 'center';
this.text.rotation = 0;
if (this.covered && App.mode !== AppMode.Edit)
return;
switch (this.type)
case HexType.Normal: this.drawTextNormal(); break;
case HexType.Blue: this.drawTextBlue(); break;
case HexType.Invisible: this.drawTextInvisible(); break;
private drawTextNormal()
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
this.text.color = Settings.hexColors.normal.text;
if (this.normalCountType === HexCountType.Invisible)
this.text.text = '?';
else
this.text.text = this.surroundCount.toString();
if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
let consecutive = true;
if (this.surroundCount < 5)
let i = 0;
for (;
this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
i++);
for (;
this.neighbors[i] === undefined
if (consecutive)
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private drawTextBlue()
if (this.blueCountType !== HexCountType.Invisible)
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
this.text.color = Settings.hexColors.blue.text;
this.text.text = this.extendCount.toString();
private drawTextInvisible()
if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
const size = App.board.hexSize;
const d = (6 - this.sideCountDirection) % 6;
const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
this.text.x += pOffset.x;
this.text.y += pOffset.y;
this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
this.text.color = Settings.hexColors.invisible.text;
this.text.text = this.sideCount[this.sideCountDirection].toString();
if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
const i = this.sideCountDirection;
let c = 0;
let n = this.neighbors[i];
while (n !== undefined && n.type !== HexType.Blue)
n = n.neighbors[i];
while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
if (n.type === HexType.Blue)
c++;
n = n.neighbors[i];
if (c === this.sideCount[i])
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private addListeners()
this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
if (event.nativeEvent.which === 1)
this.handleLeftClick(event);
else if (event.nativeEvent.which === 3)
this.handleRightClick(event);
);
private handleLeftClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
this.cycleType();
else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
this.covered = !this.covered;
this.draw();
App.stage.update();
private handleRightClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
this.cycleSideCountSide();
else
this.cycleCountType();
Orientation.ts
export enum OrientationType
Pointy,
Flat,
export class Orientation
public static pointy: Orientation = new Orientation(
Math.sqrt(3),
Math.sqrt(3) / 2,
0,
3 / 2,
Math.sqrt(3) / 3,
-1 / 3,
0,
2 / 3,
0.5,
);
public static flat: Orientation = new Orientation(
3 / 2,
0,
Math.sqrt(3) / 2,
Math.sqrt(3),
2 / 3,
0,
-1 / 3,
Math.sqrt(3) / 3,
0,
);
constructor(
public f0: number,
public f1: number,
public f2: number,
public f3: number,
public b0: number,
public b1: number,
public b2: number,
public b3: number,
public startAngle: number,
)
Point.ts
export class Point
constructor(
public x: number,
public y: number,
)
Settings.ts
// Is this a bad idea?
export class Settings
public static hexColors =
blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
invisible : border: "transparent", fill: "transparent", text: "#464646" ,
invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
;
public static hexFontScales =
blue : 0.72,
invisible : 0.64,
normal : 0.72,
;
Size.ts
export class Size
public static square(width: number)
return new Size(width, width);
constructor(
public width: number,
public height: number,
)
public scale(value: number)
return new Size(this.width * value, this.height * value);
game typescript
This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.
HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.
The following controls have been implemented thus far:
- E: switch between edit and init modes
- Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode
- Right-click: cycle the number type
- Shift+right-click an empty cell: cycle which side displays a number
A live implementation can be found here.
The hex grid implementation is largely based on this excellent resource at Red Blob Games.
After poking around at various libraries, I decided to use CreateJS for the drawing.
App.ts
/* tslint:disable:no-console */
import 'lib/easeljs';
import Board from './Board';
import HexBase from './HexBase';
import OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
// Should enums get their own file?
export enum AppMode
Edit,
EditInit,
Play,
export class App
public static canvas: HTMLCanvasElement;
public static stage: createjs.Stage;
public static board: Board;
public static mode: AppMode = AppMode.Edit;
public static width(): number
return this.canvas.clientWidth * 0.9;
public static height(): number
return this.canvas.clientHeight * 0.9;
public static start(canvas: HTMLCanvasElement)
App.canvas = canvas;
App.stage = new createjs.Stage(canvas);
App.board = new Board(new Size(10, 10));
for (const key in App.board.hexes)
if (App.board.hexes.hasOwnProperty(key))
App.board.hexes[key].randomize();
// App.board.import(App.importString);
canvas.oncontextmenu = () => false;
window.addEventListener('resize', () =>
App.draw();
);
window.addEventListener('keypress', (event: KeyboardEvent) =>
if (event.key === 'e')
if (App.mode !== AppMode.Edit)
App.mode = AppMode.Edit;
else
App.mode = AppMode.EditInit;
App.board.redraw();
App.stage.update();
);
App.board.draw();
App.draw();
public static draw()
App.canvas.width = App.canvas.parentElement.offsetWidth;
App.canvas.height = App.canvas.parentElement.offsetHeight;
App.board.resize();
App.board.redraw();
App.stage.update();
Board.ts
import 'lib/easeljs';
import App from './App';
import Hex from './Hex';
import HexBase from './HexBase';
import Orientation, OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';
export class Board
public background: createjs.Shape = new createjs.Shape();
public hexes: [key: string]: Hex = ;
public author: string;
public title: string;
public info: string;
public hexSize: Size;
private orientation = Orientation.flat;
private origin: Point = new Point(0, 0);
public constructor(
public hexCount: Size,
)
this.init(hexCount.width, hexCount.height);
public init(width: number, height: number)
this.hexCount = new Size(width, height);
this.hexes = ;
this.resize();
const i1 = -Math.floor(height / 2);
const i2 = i1 + height;
const j1 = -Math.floor(width / 2);
const j2 = j1 + width;
for (let j = j1; j < j2; j++)
const jOffset = -Math.floor(j / 2);
for (let i = i1 + jOffset; i < i2 + jOffset; i++)
const hex = HexBase.fromSQ(i, j);
this.hexes[hex.hash()] = Hex.fromHexBase(hex);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
hex.neighbors = ;
for (let i = 0; i < HexBase.directions.length; i++)
const n = hex.neighbor(i);
hex.neighbors.push(this.getHex(n.q, n.r));
public getHex(q: number, r: number): Hex
return this.hexes[HexBase.fromQR(q, r).hash()];
public resize()
this.hexSize = Size.square(Math.min(
Math.floor(App.width() / this.hexCount.width / 1.5),
Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
));
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
public redraw()
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
this.hexes[key].draw();
public draw()
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
App.stage.addChild(this.hexes[key].shape);
App.stage.addChild(this.hexes[key].text);
this.hexes[key].draw();
public import(s: string): boolean
const lineStrings = s.split('n');
if (lineStrings.length < 6)
return false;
const title = lineStrings[1];
const author = lineStrings[2];
let info = lineStrings[3];
if (lineStrings[4].replace(/s/, '').length > 0)
info += 'n' + lineStrings[4];
lineStrings.splice(0, 5);
for (let i = lineStrings.length - 1; i >= 0; i--)
lineStrings[i] = lineStrings[i].replace(/s/, '');
if (lineStrings[i].length === 0)
lineStrings.splice(i, 1);
if (lineStrings.length === 0)
return false;
const len = lineStrings[0].length;
if (len % 2 !== 0)
return false;
for (const lineString of lineStrings)
if (lineString.length !== len)
return false;
if (/[^oOxX\
this.init(len / 2, Math.ceil(lineStrings.length / 2));
const lines = ;
for (let k = -1; k < lineStrings.length; k += 2)
let line1 = '';
if (k > -1)
line1 = lineStrings[k];
let line2 = '';
if (k + 1 < lineStrings.length)
line2 = lineStrings[k + 1];
const cOffset = -Math.floor(len / 4);
const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
for (let i = 0; i < len; i += 2)
const lineT = i % 4 === 0 ? line2 : line1;
const st = lineT === '' ? '..' : lineT.substr(i, 2);
const h = Hex.fromAxial(i / 2 + cOffset, r);
const h2 = this.getHex(h.q, h.r);
h2.import(st);
this.draw();
App.stage.update();
public hexToPixel(hex: HexBase): Point
const o = this.orientation;
const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
return new Point(x + this.origin.x, y + this.origin.y);
public pixelToHex(point: Point): HexBase
const o = this.orientation;
const p = new Point(
(point.x - this.origin.x) / this.hexSize.width,
(point.y - this.origin.y) / this.hexSize.height);
const q = o.b0 * p.x + o.b1 * p.y;
const r = o.b2 * p.x + o.b3 * p.y;
return new HexBase(q, r, -q - r);
public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideOffset(side: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));
public hexSideAngle(side: number, size: Size = this.hexSize): number
const o = this.orientation;
return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
const corners: Point = ;
const center = this.hexToPixel(hex);
for (let i = 0; i < 6; i++)
const offset = this.hexCornerOffset(i, size);
corners.push(new Point(center.x + offset.x, center.y + offset.y));
return corners;
private centerOffset(): Point
const c1 = new Point(0, 0);
const c2 = new Point(0, 0);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
const corners = this.hexCorners(hex);
for (const corner of corners)
if (corner.x < c1.x) c1.x = corner.x;
if (corner.y < c1.y) c1.y = corner.y;
if (corner.x > c2.x) c2.x = corner.x;
if (corner.y > c2.y) c2.y = corner.y;
return new Point(
Math.round((c2.x - Math.abs(c1.x)) / 2),
Math.round((c2.y - Math.abs(c1.y)) / 2),
);
HexBase.ts
/* tslint:disable:no-bitwise */
import 'lib/easeljs';
import Point from './Point';
export class HexBase
public static directions: HexBase = [
new HexBase(1, 0, -1),
new HexBase(1, -1, 0),
new HexBase(0, -1, 1),
new HexBase(-1, 0, 1),
new HexBase(-1, 1, 0),
new HexBase(0, 1, -1),
];
public static fromQR(q: number, r: number): HexBase
return new HexBase(q, r, -q - r);
public static fromQS(q: number, s: number): HexBase
return new HexBase(q, -q - s, s);
public static fromRS(r: number, s: number): HexBase
return new HexBase(-r - s, r, s);
public static fromRQ(r: number, q: number): HexBase
return new HexBase(q, r, -q - r);
public static fromSQ(s: number, q: number): HexBase
return new HexBase(q, -q - s, s);
public static fromSR(s: number, r: number): HexBase
return new HexBase(-r - s, r, s);
public static fromAxial(col: number, row: number): HexBase
const q = col;
const s = row - (col - (col & 1)) / 2;
return HexBase.fromQS(q, s);
public sideCount: number = ;
public surroundCount: number = 0;
public extendCount: number = 0;
constructor(
public q: number,
public r: number,
public s: number,
)
for (let i = 0; i < 6; i++)
this.sideCount.push(0);
public toAxial(): Point
const col = this.q;
const row = this.s + (this.q - (this.q & 1)) / 2;
return new Point(col, row);
public hash(): string
return this.q + ',' + this.r;
public add(h: HexBase): HexBase
return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);
public neighbor(direction: number): HexBase
return this.add(HexBase.directions[direction]);
public round(): HexBase
let q = Math.floor(Math.round(this.q));
let r = Math.floor(Math.round(this.r));
let s = Math.floor(Math.round(this.s));
const qD = Math.abs(q - this.q);
const rD = Math.abs(r - this.r);
const sD = Math.abs(s - this.s);
if (qD > rD && qD > sD)
q = -r - s;
else
if (rD > sD)
r = -q - s;
else
s = -q - r;
return new HexBase(q, r, s);
Hex.ts
import App, AppMode from './App';
import HexBase from './HexBase';
import Point from './Point';
import Settings from './Settings';
export enum HexType
Invisible,
Normal,
Blue,
export enum HexCountType
Plain,
Invisible,
Extended,
export class Hex extends HexBase
public static fromHexBase(h: HexBase): Hex
return new Hex(h.q, h.r, h.s);
public type: HexType = HexType.Invisible;
public normalCountType: HexCountType = HexCountType.Plain;
public blueCountType: HexCountType = HexCountType.Invisible;
public invisibleCountType: HexCountType = HexCountType.Plain;
public shape: createjs.Shape = new createjs.Shape();
public text: createjs.Text = new createjs.Text();
public covered: boolean = false;
public sideCountDirection: number = -1;
public neighbors: Hex = ;
constructor(
public q: number,
public r: number,
public s: number,
)
super(q, r, s);
this.addListeners();
public updateCounts(amount: number)
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
n.surroundCount += amount;
n.extendCount += amount;
if (n.neighbors[i] !== undefined)
n.neighbors[i].extendCount += amount;
let j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].extendCount += amount;
j = (i - 3 + 6) % 6;
while (n !== undefined)
n.sideCount[j] += amount;
n = n.neighbors[i];
public cycleType()
let newType = HexType.Normal;
switch (this.type)
case HexType.Normal: newType = HexType.Blue; break;
case HexType.Blue: newType = HexType.Invisible; break;
this.changeType(newType);
this.drawAllNeighbors();
this.draw();
App.stage.update();
public cycleSideCountSide()
if (this.type !== HexType.Invisible)
return;
let i = this.sideCountDirection;
i = i === -1 ? 5 : i - 1;
while (i !== -1 && this.neighbors[i] === undefined)
i = i === -1 ? 5 : i - 1;
this.sideCountDirection = i;
if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
this.invisibleCountType = HexCountType.Plain;
this.draw();
App.stage.update();
public cycleCountType()
switch (this.type)
case HexType.Normal:
switch (this.normalCountType)
case HexCountType.Invisible:
this.normalCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.surroundCount > 1)
this.normalCountType = HexCountType.Extended;
else
this.normalCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.normalCountType = HexCountType.Invisible;
break;
case HexType.Blue:
if (this.blueCountType === HexCountType.Plain)
this.blueCountType = HexCountType.Invisible;
else
this.blueCountType = HexCountType.Plain;
break;
case HexType.Invisible:
if (this.sideCountDirection === -1)
this.cycleSideCountSide();
else
switch (this.invisibleCountType)
case HexCountType.Invisible:
this.invisibleCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.sideCount[this.sideCountDirection] > 1)
this.invisibleCountType = HexCountType.Extended;
else
this.invisibleCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.invisibleCountType = HexCountType.Invisible;
break;
break;
this.draw();
App.stage.update();
public drawAllNeighbors()
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
if (n.neighbors[i] !== undefined)
n.neighbors[i].draw();
const j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].draw();
while (n !== undefined)
n.draw();
n = n.neighbors[i];
public draw()
const p = App.board.hexToPixel(this);
this.drawHex(p);
this.drawText(p);
public changeType(type: HexType)
const prevType = this.type;
this.type = type;
if (prevType === HexType.Blue && type !== HexType.Blue)
this.updateCounts(-1);
else if (prevType !== HexType.Blue && type === HexType.Blue)
this.updateCounts(1);
public changeCountType(type: HexCountType)
switch (this.type)
case HexType.Normal:
this.normalCountType = type;
break;
case HexType.Blue:
this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
break;
case HexType.Invisible:
this.invisibleCountType = type;
break;
public randomize()
const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
this.changeType(types[Math.floor(Math.random() * 3)]);
const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);
public import(s: string)
const typeS = s.substr(0, 1);
const countS = s.substr(1, 1);
this.covered = false;
switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;
switch (countS)
case '.': this.changeCountType(HexCountType.Invisible); break;
case '+': this.changeCountType(HexCountType.Plain); break;
case 'c': this.changeCountType(HexCountType.Extended); break;
case 'n': this.changeCountType(HexCountType.Extended); break;
private color()
let color = Settings.hexColors.normal;
if (this.covered && App.mode !== AppMode.Edit)
color = Settings.hexColors.covered;
else
switch (this.type)
case HexType.Invisible:
if (App.mode === AppMode.Edit)
color = Settings.hexColors.invisibleEditing;
else
color = Settings.hexColors.invisible;
break;
case HexType.Blue:
color = Settings.hexColors.blue;
break;
return color;
private drawHex(p: Point)
const color = this.color();
const g = this.shape.graphics;
g.clear();
const size = App.board.hexSize;
g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);
private drawText(p: Point)
this.text.x = p.x;
this.text.y = p.y;
this.text.text = '';
this.text.textBaseline = 'middle';
this.text.textAlign = 'center';
this.text.rotation = 0;
if (this.covered && App.mode !== AppMode.Edit)
return;
switch (this.type)
case HexType.Normal: this.drawTextNormal(); break;
case HexType.Blue: this.drawTextBlue(); break;
case HexType.Invisible: this.drawTextInvisible(); break;
private drawTextNormal()
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
this.text.color = Settings.hexColors.normal.text;
if (this.normalCountType === HexCountType.Invisible)
this.text.text = '?';
else
this.text.text = this.surroundCount.toString();
if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
let consecutive = true;
if (this.surroundCount < 5)
let i = 0;
for (;
this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
i++);
for (;
this.neighbors[i] === undefined
if (consecutive)
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private drawTextBlue()
if (this.blueCountType !== HexCountType.Invisible)
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
this.text.color = Settings.hexColors.blue.text;
this.text.text = this.extendCount.toString();
private drawTextInvisible()
if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
const size = App.board.hexSize;
const d = (6 - this.sideCountDirection) % 6;
const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
this.text.x += pOffset.x;
this.text.y += pOffset.y;
this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
this.text.color = Settings.hexColors.invisible.text;
this.text.text = this.sideCount[this.sideCountDirection].toString();
if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
const i = this.sideCountDirection;
let c = 0;
let n = this.neighbors[i];
while (n !== undefined && n.type !== HexType.Blue)
n = n.neighbors[i];
while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
if (n.type === HexType.Blue)
c++;
n = n.neighbors[i];
if (c === this.sideCount[i])
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';
private addListeners()
this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
if (event.nativeEvent.which === 1)
this.handleLeftClick(event);
else if (event.nativeEvent.which === 3)
this.handleRightClick(event);
);
private handleLeftClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
this.cycleType();
else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
this.covered = !this.covered;
this.draw();
App.stage.update();
private handleRightClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
this.cycleSideCountSide();
else
this.cycleCountType();
Orientation.ts
export enum OrientationType
Pointy,
Flat,
export class Orientation
public static pointy: Orientation = new Orientation(
Math.sqrt(3),
Math.sqrt(3) / 2,
0,
3 / 2,
Math.sqrt(3) / 3,
-1 / 3,
0,
2 / 3,
0.5,
);
public static flat: Orientation = new Orientation(
3 / 2,
0,
Math.sqrt(3) / 2,
Math.sqrt(3),
2 / 3,
0,
-1 / 3,
Math.sqrt(3) / 3,
0,
);
constructor(
public f0: number,
public f1: number,
public f2: number,
public f3: number,
public b0: number,
public b1: number,
public b2: number,
public b3: number,
public startAngle: number,
)
Point.ts
export class Point
constructor(
public x: number,
public y: number,
)
Settings.ts
// Is this a bad idea?
export class Settings
public static hexColors =
blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
invisible : border: "transparent", fill: "transparent", text: "#464646" ,
invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
;
public static hexFontScales =
blue : 0.72,
invisible : 0.64,
normal : 0.72,
;
Size.ts
export class Size
public static square(width: number)
return new Size(width, width);
constructor(
public width: number,
public height: number,
)
public scale(value: number)
return new Size(this.width * value, this.height * value);
game typescript
asked May 2 at 19:25
Jason Clement
1163
1163
add a comment |Â
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193494%2fthe-beginnings-of-a-hexcells-editor%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password