mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +00:00
864 lines
23 KiB
TypeScript
864 lines
23 KiB
TypeScript
/// <reference path="../DefinitelyTyped/react/react-global.d.ts" />
|
|
|
|
// Fixture taken from https://github.com/RyanCavanaugh/koany/blob/master/koany.tsx
|
|
|
|
interface Garden {
|
|
colors: Gardens.RockColor[];
|
|
shapes: Gardens.RockShape[];
|
|
}
|
|
|
|
namespace Gardens {
|
|
export enum RockShape {
|
|
Empty,
|
|
Circle,
|
|
Triangle,
|
|
Square,
|
|
Max
|
|
}
|
|
export const RockShapes = [RockShape.Circle, RockShape.Triangle, RockShape.Square];
|
|
export const RockShapesAndEmpty = RockShapes.concat(RockShape.Empty);
|
|
|
|
export enum RockColor {
|
|
Empty,
|
|
White,
|
|
Red,
|
|
Black,
|
|
Max
|
|
}
|
|
export const RockColors = [RockColor.White, RockColor.Red, RockColor.Black];
|
|
export const RockColorsAndEmpty = RockColors.concat(RockColor.Empty);
|
|
|
|
export const Size = 9;
|
|
|
|
// 012
|
|
// 345
|
|
// 678
|
|
export const adjacencies = [
|
|
[1, 3], [0, 4, 2], [1, 5],
|
|
[0, 4, 6], [3, 1, 7, 5], [2, 4, 8],
|
|
[3, 7], [6, 4, 8], [7, 5]
|
|
];
|
|
}
|
|
|
|
module Koan {
|
|
export enum DescribeContext {
|
|
// every "white stone" is ...
|
|
Singular,
|
|
// all "white stones" are
|
|
Plural,
|
|
// every stone in the top row is "white"
|
|
Adjectival
|
|
}
|
|
|
|
export enum PartType {
|
|
Selector,
|
|
Aspect
|
|
}
|
|
|
|
export enum StateTestResult {
|
|
Fail = 0,
|
|
WeakPass = 1,
|
|
Pass = 2
|
|
}
|
|
|
|
/// A general format for producing a Statement
|
|
export interface StatementTemplate<T> {
|
|
holes: PartType[];
|
|
describe(args: T): string;
|
|
test(g: Garden, args: T): StateTestResult;
|
|
}
|
|
|
|
/// A completed rule that can be used to test a Garden
|
|
export interface ProducedStatement<T> {
|
|
test(g: Garden): StateTestResult;
|
|
description: string;
|
|
children: T;
|
|
|
|
hasPassedAndFailed(): boolean;
|
|
}
|
|
|
|
function rnd(max: number) {
|
|
return Math.floor(Math.random() * max);
|
|
}
|
|
|
|
function randomColor(): Gardens.RockColor {
|
|
return Math.floor(Math.random() * (Gardens.RockColor.Max - 1)) + 1
|
|
}
|
|
|
|
function randomShape(): Gardens.RockShape {
|
|
return Math.floor(Math.random() * (Gardens.RockShape.Max - 1)) + 1
|
|
}
|
|
|
|
/* New Impl Here */
|
|
interface SelectorSpec<T> {
|
|
childTypes?: PartType[];
|
|
precedence: number;
|
|
weight: number;
|
|
test(args: T, g: Garden, index: number): string|number|boolean;
|
|
describe(args: T, context: DescribeContext): string;
|
|
isAllValues(values: Array<string>|Array<number>): boolean;
|
|
}
|
|
|
|
interface ProducedSelector {
|
|
test(g: Garden, index: number): string|number|boolean;
|
|
getDescription(plural: DescribeContext): string;
|
|
seenAllValues(): boolean;
|
|
}
|
|
|
|
export function buildSelector<T>(spec: SelectorSpec<T>, args: T): ProducedSelector {
|
|
let seenResults: { [s: string]: boolean;} = {};
|
|
return {
|
|
test: (g: Garden, index: number) => {
|
|
var result = spec.test(args, g, index);
|
|
seenResults[result + ''] = true;
|
|
return result;
|
|
},
|
|
getDescription: (context) => {
|
|
return spec.describe(args, context);
|
|
},
|
|
seenAllValues: () => {
|
|
return spec.isAllValues(Object.keys(seenResults));
|
|
}
|
|
}
|
|
}
|
|
|
|
export var SelectorTemplates: Array<SelectorSpec<{}>> = [];
|
|
module LetsMakeSomeSelectors {
|
|
// Is rock
|
|
SelectorTemplates.push({
|
|
test: (args, g, i) => g.colors[i] !== Gardens.RockColor.Empty,
|
|
describe: (args, context) => {
|
|
switch(context) {
|
|
case DescribeContext.Plural:
|
|
return 'Stones';
|
|
case DescribeContext.Adjectival:
|
|
return 'not empty';
|
|
case DescribeContext.Singular:
|
|
return 'Stone';
|
|
}
|
|
},
|
|
isAllValues: items => items.length === 2,
|
|
precedence: 0,
|
|
weight: 1
|
|
});
|
|
|
|
// Is of a certain color and/or shape
|
|
Gardens.RockColorsAndEmpty.forEach(color => {
|
|
let colorName = Gardens.RockColor[color];
|
|
let colorWeight = color === Gardens.RockColor.Empty ? 1 : 0.33;
|
|
Gardens.RockShapesAndEmpty.forEach(shape => {
|
|
let shapeName = Gardens.RockShape[shape];
|
|
let shapeWeight = shape === Gardens.RockShape.Empty ? 1 : 0.33;
|
|
SelectorTemplates.push({
|
|
test: (args, g, i) => {
|
|
if(color === Gardens.RockColor.Empty) {
|
|
if (shape === Gardens.RockShape.Empty) {
|
|
return g.colors[i] === Gardens.RockColor.Empty;
|
|
} else {
|
|
return g.shapes[i] === shape;
|
|
}
|
|
} else {
|
|
if (shape === Gardens.RockShape.Empty) {
|
|
return g.colors[i] === color;
|
|
} else {
|
|
return g.shapes[i] === shape && g.colors[i] === color;
|
|
}
|
|
}
|
|
},
|
|
describe: (args, context) => {
|
|
if(color === Gardens.RockColor.Empty) {
|
|
if (shape === Gardens.RockShape.Empty) {
|
|
switch(context) {
|
|
case DescribeContext.Plural:
|
|
return 'Empty Cells';
|
|
case DescribeContext.Adjectival:
|
|
return 'Empty';
|
|
case DescribeContext.Singular:
|
|
return 'Empty Cell';
|
|
}
|
|
} else {
|
|
switch(context) {
|
|
case DescribeContext.Plural:
|
|
return shapeName + 's';
|
|
case DescribeContext.Adjectival:
|
|
return 'a ' + shapeName;
|
|
case DescribeContext.Singular:
|
|
return shapeName;
|
|
}
|
|
}
|
|
} else {
|
|
if (shape === Gardens.RockShape.Empty) {
|
|
switch(context) {
|
|
case DescribeContext.Plural:
|
|
return colorName + ' Stones';
|
|
case DescribeContext.Adjectival:
|
|
return colorName;
|
|
case DescribeContext.Singular:
|
|
return colorName + ' Stone';
|
|
}
|
|
} else {
|
|
switch(context) {
|
|
case DescribeContext.Plural:
|
|
return colorName + ' ' + shapeName + 's';
|
|
case DescribeContext.Adjectival:
|
|
return 'a ' + colorName + ' ' + shapeName;
|
|
case DescribeContext.Singular:
|
|
return colorName + ' ' + shapeName;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
isAllValues: items => items.length === 2,
|
|
precedence: 3,
|
|
weight: (shapeWeight + colorWeight === 2) ? 0.3 : shapeWeight * colorWeight
|
|
});
|
|
});
|
|
});
|
|
|
|
// [?] in the [top|middle|bottom] [row|column]
|
|
[0, 1, 2].forEach(rowCol => {
|
|
[true, false].forEach(isRow => {
|
|
var name = (isRow ? ['top', 'middle', 'bottom'] : ['left', 'middle', 'right'])[rowCol] + ' ' + (isRow ? 'row' : 'column');
|
|
var spec: SelectorSpec<[ProducedSelector]> = {
|
|
childTypes: [PartType.Selector],
|
|
test: (args, g, i) => {
|
|
var c = isRow ? Math.floor(i / 3) : i % 3;
|
|
if (c === rowCol) {
|
|
return args[0].test(g, i);
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
describe: (args, plural) => args[0].getDescription(plural) + ' in the ' + name,
|
|
isAllValues: items => items.length === 2,
|
|
precedence: 4,
|
|
weight: 1 / 6
|
|
};
|
|
SelectorTemplates.push(spec);
|
|
});
|
|
});
|
|
|
|
// [?] next to a [?]
|
|
SelectorTemplates.push({
|
|
childTypes: [PartType.Selector, PartType.Selector],
|
|
test: (args, g, i) => {
|
|
if (args[0].test(g, i)) {
|
|
return Gardens.adjacencies[i].some(x => !!args[1].test(g, x));
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
describe: (args, plural) => {
|
|
return args[0].getDescription(plural) + ' next to a ' + args[1].getDescription(DescribeContext.Singular);
|
|
},
|
|
isAllValues: items => items.length === 2,
|
|
precedence: 4,
|
|
weight: 1
|
|
} as SelectorSpec<[ProducedSelector, ProducedSelector]>);
|
|
}
|
|
|
|
export function buildStatement<T>(s: StatementTemplate<T>, args: T): ProducedStatement<T> {
|
|
let hasPassed = false;
|
|
let hasFailed = false;
|
|
|
|
let result: ProducedStatement<T> = {
|
|
children: args,
|
|
description: s.describe(args),
|
|
test: (g) => {
|
|
let r = s.test(g, args);
|
|
if (r === StateTestResult.Pass) {
|
|
hasPassed = true;
|
|
} else if(r === StateTestResult.Fail) {
|
|
hasFailed = true;
|
|
}
|
|
return r;
|
|
},
|
|
hasPassedAndFailed: () => {
|
|
return hasPassed && hasFailed && (args as any as ProducedSelector[]).every(c => c.seenAllValues());
|
|
}
|
|
};
|
|
return result;
|
|
}
|
|
|
|
export let StatementList: StatementTemplate<any>[] = [];
|
|
module LetsMakeSomeStatements {
|
|
// Every [?] is a [?]
|
|
StatementList.push({
|
|
holes: [PartType.Selector, PartType.Selector],
|
|
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
|
let didAnyTests = false;
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
if (args[0].test(g, i)) {
|
|
if(!args[1].test(g, i)) return StateTestResult.Fail;
|
|
didAnyTests = true;
|
|
}
|
|
}
|
|
return didAnyTests ? StateTestResult.Pass : StateTestResult.WeakPass;
|
|
},
|
|
describe: args => {
|
|
return 'Every ' + args[0].getDescription(DescribeContext.Singular) + ' is ' + args[1].getDescription(DescribeContext.Adjectival);
|
|
}
|
|
});
|
|
|
|
// There is exactly 1 [?]
|
|
StatementList.push({
|
|
holes: [PartType.Selector],
|
|
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
|
var count = 0;
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
if (args[0].test(g, i)) count++;
|
|
}
|
|
|
|
return count === 1 ? StateTestResult.Pass : StateTestResult.Fail;
|
|
},
|
|
describe: args => {
|
|
return 'There is exactly one ' + args[0].description;
|
|
}
|
|
});
|
|
|
|
// There are more [?] than [?]
|
|
StatementList.push({
|
|
holes: [PartType.Selector, PartType.Selector],
|
|
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
|
var p1c = 0, p2c = 0;
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
if (args[0].test(g, i)) p1c++;
|
|
if (args[1].test(g, i)) p2c++;
|
|
}
|
|
if(p1c > p2c && p2c > 0) {
|
|
return StateTestResult.Pass;
|
|
} else if(p1c > p2c) {
|
|
return StateTestResult.WeakPass;
|
|
} else {
|
|
return StateTestResult.Fail;
|
|
}
|
|
},
|
|
describe: args => {
|
|
return 'There are more ' + args[0].descriptionPlural + ' than ' + args[1].descriptionPlural;
|
|
}
|
|
});
|
|
}
|
|
|
|
function randomElementOf<T>(arr: T[]): T {
|
|
if (arr.length === 0) {
|
|
return undefined;
|
|
} else {
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
}
|
|
}
|
|
|
|
function randomWeightedElementOf<T extends { weight: number }>(arr: T[]): T {
|
|
var totalWeight = arr.reduce((acc, v) => acc + v.weight, 0);
|
|
var rnd = Math.random() * totalWeight;
|
|
for (var i = 0; i < arr.length; i++) {
|
|
rnd -= arr[i].weight;
|
|
if (rnd <= 0) return arr[i];
|
|
}
|
|
// Got destroyed by floating error, just try again
|
|
return randomWeightedElementOf(arr);
|
|
}
|
|
|
|
export function buildRandomNewSelector(maxPrecedence = 1000000): ProducedSelector {
|
|
var choices = SelectorTemplates;
|
|
|
|
let initial = randomWeightedElementOf(choices.filter(p => p.precedence <= maxPrecedence));
|
|
// Fill in the holes
|
|
if (initial.childTypes) {
|
|
var fills = initial.childTypes.map(h => {
|
|
if (h === PartType.Selector) {
|
|
return buildRandomNewSelector(initial.precedence - 1);
|
|
} else {
|
|
throw new Error('Only know how to fill Selector holes')
|
|
}
|
|
});
|
|
return buildSelector(initial, fills);
|
|
} else {
|
|
return buildSelector(initial, []);
|
|
}
|
|
}
|
|
|
|
export function makeEmptyGarden(): Garden {
|
|
var g = {} as Garden;
|
|
g.colors = [];
|
|
g.shapes = [];
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
g.colors.push(Gardens.RockColor.Empty);
|
|
g.shapes.push(Gardens.RockShape.Empty);
|
|
}
|
|
|
|
return g;
|
|
}
|
|
|
|
export function gardenToString(g: Garden): string {
|
|
return g.colors.join('') + g.shapes.join('');
|
|
}
|
|
|
|
export function makeRandomGarden(): Garden {
|
|
var g = makeEmptyGarden();
|
|
blitRandomGardenPair(g, g);
|
|
return g;
|
|
}
|
|
|
|
export function cloneGarden(g: Garden): Garden {
|
|
var result: Garden = {
|
|
colors: g.colors.slice(0),
|
|
shapes: g.shapes.slice(0)
|
|
};
|
|
return result;
|
|
}
|
|
|
|
export function clearGarden(g: Garden) {
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
g.colors[i] = Gardens.RockColor.Empty;
|
|
g.shapes[i] = Gardens.RockShape.Empty;
|
|
}
|
|
}
|
|
|
|
export function blitRandomGardenPair(g1: Garden, g2: Garden): void {
|
|
let placeCount = 0;
|
|
for (var i = 0; i < Gardens.Size; i++) {
|
|
if (rnd(7) === 0) {
|
|
g1.colors[i] = g2.colors[i] = randomColor();
|
|
g1.shapes[i] = g2.shapes[i] = randomShape();
|
|
} else {
|
|
placeCount++;
|
|
g1.colors[i] = g2.colors[i] = Gardens.RockColor.Empty;
|
|
g1.shapes[i] = g2.shapes[i] = Gardens.RockShape.Empty;
|
|
}
|
|
}
|
|
if (placeCount === 0) blitRandomGardenPair(g1, g2);
|
|
}
|
|
|
|
export function blitNumberedGarden(g: Garden, stoneCount: number, n: number): void {
|
|
clearGarden(g);
|
|
|
|
let cellNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
|
for (let i = 0; i < stoneCount; i++) {
|
|
let cellNum = getValue(cellNumbers.length);
|
|
let cell = cellNumbers.splice(cellNum, 1)[0];
|
|
g.colors[cell] = getValue(3) + 1;
|
|
g.shapes[cell] = getValue(3) + 1;
|
|
}
|
|
|
|
function getValue(max: number) {
|
|
let result = n % max;
|
|
n = (n - result) / max;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
export function mutateGarden(g: Garden): void {
|
|
while (true) {
|
|
var op = rnd(5);
|
|
let x = rnd(Gardens.Size);
|
|
let y = rnd(Gardens.Size);
|
|
switch (op) {
|
|
case 0: // Swap two non-identical cells
|
|
if (g.colors[x] !== g.colors[y] || g.shapes[x] !== g.shapes[y]) {
|
|
var tmp: any = g.colors[x];
|
|
g.colors[x] = g.colors[y];
|
|
g.colors[y] = tmp;
|
|
tmp = g.shapes[x];
|
|
g.shapes[x] = g.shapes[y];
|
|
g.shapes[y] = tmp;
|
|
return;
|
|
}
|
|
break;
|
|
case 1: // Add a stone
|
|
if (g.colors[x] === Gardens.RockColor.Empty) {
|
|
g.colors[x] = randomColor();
|
|
g.shapes[x] = randomShape();
|
|
return;
|
|
}
|
|
break;
|
|
case 2: // Remove a stone
|
|
if (g.colors.filter(x => x !== Gardens.RockColor.Empty).length === 1) continue;
|
|
|
|
if (g.colors[x] !== Gardens.RockColor.Empty) {
|
|
g.colors[x] = Gardens.RockColor.Empty;
|
|
g.shapes[x] = Gardens.RockShape.Empty;
|
|
return;
|
|
}
|
|
break;
|
|
case 3: // Change a color
|
|
let c = randomColor();
|
|
if (g.colors[x] !== Gardens.RockColor.Empty && g.colors[x] !== c) {
|
|
g.colors[x] = c;
|
|
return;
|
|
}
|
|
break;
|
|
case 4: // Change a shape
|
|
let s = randomShape();
|
|
if (g.shapes[x] !== Gardens.RockShape.Empty && g.shapes[x] !== s) {
|
|
g.shapes[x] = s;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Indexion {
|
|
sizes: number[];
|
|
constructor(...sizes: number[]) {
|
|
this.sizes = sizes;
|
|
}
|
|
|
|
public getValues(index: number): number[] {
|
|
let result = new Array<number>(this.sizes.length);
|
|
this.fillValues(index, result);
|
|
return result;
|
|
}
|
|
|
|
public fillValues(index: number, result: number[]): void {
|
|
for (var i = 0; i < this.sizes.length; i++) {
|
|
result[i] = index % this.sizes[i];
|
|
index -= result[i];
|
|
index /= this.sizes[i];
|
|
}
|
|
}
|
|
|
|
public valuesToIndex(values: number[]): number {
|
|
var result = 0;
|
|
var factor = 1;
|
|
for (var i = 0; i < this.sizes.length; i++) {
|
|
result += values[i] * this.sizes[i] * factor;
|
|
factor *= this.sizes[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public getAdjacentIndices(index: number): number[][] {
|
|
var baseline = this.getValues(index);
|
|
var results: number[][] = [];
|
|
for (var i = 0; i < this.sizes.length; i++) {
|
|
if(baseline[i] > 0) {
|
|
baseline[i]--;
|
|
results.push(baseline.slice());
|
|
baseline[i]++;
|
|
}
|
|
if(baseline[i] < this.sizes[i] - 1) {
|
|
baseline[i]++;
|
|
results.push(baseline.slice());
|
|
baseline[i]--;
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
public distance(index1: number, index2: number): number {
|
|
let delta = 0;
|
|
for (var i = 0; i < this.sizes.length; i++) {
|
|
var a = index1 % this.sizes[i];
|
|
var b = index2 % this.sizes[i];
|
|
delta += Math.abs(b - a);
|
|
index1 -= a;
|
|
index2 -= b;
|
|
index1 /= this.sizes[i];
|
|
index2 /= this.sizes[i];
|
|
}
|
|
return delta;
|
|
}
|
|
}
|
|
|
|
|
|
function makeNewExample() {
|
|
while (true) {
|
|
var p1 = Koan.buildSelector(Koan.SelectorTemplates[12], []);
|
|
var p2 = Koan.buildSelector(Koan.SelectorTemplates[14], []);
|
|
var test = Koan.buildStatement(Koan.StatementList[0], [p1, p2]);
|
|
|
|
var examples: Garden[] = [];
|
|
|
|
console.log('Attempt to generate examples for "' + test.description + '"');
|
|
|
|
var maxGarden = /*(9 * 9) + (9 * 9 * 9 * 8) + */(9 * 9 * 9 * 8 * 9 * 7);
|
|
let g = Koan.makeEmptyGarden();
|
|
let passCount = 0, failCount = 0;
|
|
let resultLookup: boolean[] = [];
|
|
let lastResult: boolean = undefined;
|
|
for (var i = 0; i < maxGarden; i++) {
|
|
Koan.blitNumberedGarden(g, 3, i);
|
|
let result = test.test(g);
|
|
if(result === Koan.StateTestResult.Pass) {
|
|
resultLookup[i] = true;
|
|
passCount++;
|
|
|
|
if (lastResult !== true && examples.length < 10) examples.push(Koan.cloneGarden(g));
|
|
lastResult = true;
|
|
} else if (result === Koan.StateTestResult.Fail) {
|
|
resultLookup[i] = false;
|
|
failCount++;
|
|
|
|
if (lastResult !== false && examples.length < 10) examples.push(Koan.cloneGarden(g));
|
|
lastResult = false;
|
|
}
|
|
|
|
if (examples.length === 10) break;
|
|
}
|
|
|
|
console.log('Rule passes ' + passCount + ' and fails ' + failCount);
|
|
|
|
/*
|
|
if (!test.hasPassedAndFailed()) {
|
|
console.log('Rule has unreachable, contradictory, or tautological clauses');
|
|
continue;
|
|
}
|
|
|
|
if (passCount === 0 || failCount === 0) {
|
|
console.log('Rule is always true or always false');
|
|
continue;
|
|
}
|
|
*/
|
|
|
|
var h = document.createElement('h2');
|
|
h.innerText = test.description;
|
|
document.body.appendChild(h);
|
|
|
|
return { test: test, examples: examples };
|
|
}
|
|
}
|
|
|
|
let list: Garden[] = [];
|
|
let test: Koan.ProducedStatement<any>;
|
|
window.onload = function() {
|
|
let rule = makeNewExample();
|
|
let garden = Koan.makeRandomGarden();
|
|
list = rule.examples;
|
|
test = rule.test;
|
|
|
|
function renderList() {
|
|
function makeGarden(g: Garden, i: number) {
|
|
return <GardenDisplay
|
|
garden={g}
|
|
key={i + Koan.gardenToString(g)}
|
|
test={test}
|
|
leftButton='✗'
|
|
rightButton='✎'
|
|
onLeftButtonClicked={() => {
|
|
console.log(list.indexOf(g));
|
|
list.splice(list.indexOf(g), 1);
|
|
renderList();
|
|
}}
|
|
onRightButtonClicked={() => {
|
|
garden = g;
|
|
renderEditor();
|
|
}}
|
|
/>;
|
|
}
|
|
let gardenList = <div>{list.map(makeGarden)}</div>;
|
|
React.render(gardenList, document.getElementById('results'));
|
|
}
|
|
|
|
let i = 0;
|
|
function renderEditor() {
|
|
i++;
|
|
let editor = <GardenEditor key={i} test={rule.test} garden={garden} onSaveClicked={(garden) => {
|
|
list.push(garden);
|
|
renderList();
|
|
}} />;
|
|
React.render(editor, document.getElementById('editor'));
|
|
}
|
|
|
|
renderList();
|
|
renderEditor();
|
|
}
|
|
|
|
function classNames(nameMap: any): string {
|
|
return Object.keys(nameMap).filter(k => nameMap[k]).join(' ');
|
|
}
|
|
|
|
interface GardenCellProps extends React.Props<{}> {
|
|
color: Gardens.RockColor;
|
|
shape: Gardens.RockShape;
|
|
index: number;
|
|
|
|
movable?: boolean;
|
|
onEdit?(newColor: Gardens.RockColor, newShape: Gardens.RockShape): void;
|
|
}
|
|
interface GardenCellState {
|
|
isDragging?: boolean;
|
|
}
|
|
class GardenCell extends React.Component<GardenCellProps, GardenCellState> {
|
|
state: GardenCellState = {};
|
|
ignoreNextEdit = false;
|
|
|
|
render() {
|
|
var classes = ['cell', 'index_' + this.props.index];
|
|
|
|
if (this.state.isDragging) {
|
|
// Render as blank
|
|
} else {
|
|
classes.push(Gardens.RockColor[this.props.color], Gardens.RockShape[this.props.shape]);
|
|
}
|
|
|
|
if (this.props.movable) classes.push('movable');
|
|
let events: React.HTMLAttributes = {
|
|
onDragStart: (e) => {
|
|
this.ignoreNextEdit = false;
|
|
e.dataTransfer.dropEffect = 'copyMove';
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('shape', this.props.shape.toString());
|
|
e.dataTransfer.setData('color', this.props.color.toString());
|
|
|
|
let drag = document.getElementById(getGardenName(this.props.color, this.props.shape));
|
|
let xfer: any = (e.nativeEvent as DragEvent).dataTransfer;
|
|
xfer.setDragImage(drag, drag.clientWidth * 0.5, drag.clientHeight * 0.5);
|
|
|
|
this.setState({ isDragging: true });
|
|
},
|
|
onDragEnter: (e) => {
|
|
e.dataTransfer.dropEffect = 'move';
|
|
e.preventDefault();
|
|
},
|
|
onDragOver: (e) => {
|
|
e.dataTransfer.dropEffect = 'move';
|
|
e.preventDefault();
|
|
},
|
|
onDragEnd: (e) => {
|
|
this.setState({ isDragging: false });
|
|
if (!this.ignoreNextEdit) {
|
|
this.props.onEdit && this.props.onEdit(undefined, undefined);
|
|
}
|
|
},
|
|
draggable: true
|
|
}
|
|
|
|
let handleDrop = (event: React.DragEvent) => {
|
|
if(this.props.onEdit) {
|
|
if (this.state.isDragging) {
|
|
// Dragged to self, don't do anything
|
|
this.ignoreNextEdit = true;
|
|
} else {
|
|
let shape: Gardens.RockShape = +event.dataTransfer.getData('shape');
|
|
let color: Gardens.RockColor = +event.dataTransfer.getData('color');
|
|
this.props.onEdit(color, shape);
|
|
}
|
|
}
|
|
}
|
|
|
|
return <span className={classes.join(' ')} onDrop={handleDrop} {...this.props.movable ? events : {}} />;
|
|
}
|
|
}
|
|
|
|
interface GardenDisplayProps extends React.Props<GardenDisplay> {
|
|
garden?: Garden;
|
|
test?: Koan.ProducedStatement<any>;
|
|
|
|
leftButton?: string;
|
|
rightButton?: string;
|
|
onLeftButtonClicked?(): void;
|
|
onRightButtonClicked?(): void;
|
|
|
|
editable?: boolean;
|
|
onChanged?(newGarden: Garden): void;
|
|
}
|
|
interface GardenDisplayState {
|
|
garden?: Garden;
|
|
}
|
|
class GardenDisplay extends React.Component<GardenDisplayProps, GardenDisplayState> {
|
|
state = {
|
|
garden: Koan.cloneGarden(this.props.garden)
|
|
};
|
|
|
|
leftClicked = () => {
|
|
this.props.onLeftButtonClicked && this.props.onLeftButtonClicked();
|
|
};
|
|
|
|
rightClicked = () => {
|
|
this.props.onRightButtonClicked && this.props.onRightButtonClicked();
|
|
};
|
|
|
|
render() {
|
|
let g = this.state.garden;
|
|
let pass = (this.props.test && this.props.test.test(this.state.garden));
|
|
|
|
let classes = {
|
|
garden: true,
|
|
unknown: pass === undefined,
|
|
pass: pass === Koan.StateTestResult.Pass || pass === Koan.StateTestResult.WeakPass,
|
|
fail: pass === Koan.StateTestResult.Fail,
|
|
editable: this.props.editable
|
|
};
|
|
|
|
var children = g.colors.map((_, i) => (
|
|
<GardenCell
|
|
key={i}
|
|
color={g.colors[i]}
|
|
shape={g.shapes[i]}
|
|
index={i}
|
|
movable={this.props.editable}
|
|
onEdit={(newColor, newShape) => {
|
|
if(this.props.editable) {
|
|
let newGarden = Koan.cloneGarden(this.state.garden);
|
|
newGarden.colors[i] = newColor;
|
|
newGarden.shapes[i] = newShape;
|
|
this.setState({ garden: newGarden });
|
|
this.props.onChanged && this.props.onChanged(newGarden);
|
|
}
|
|
}}
|
|
/>));
|
|
|
|
return <div className="gardenDisplay">
|
|
<div className={classNames(classes)}>{children}</div>
|
|
<span className="infoRow">
|
|
{this.props.leftButton && <div className="button left" onClick={this.leftClicked}>{this.props.leftButton}</div>}
|
|
<div className={"passfail " + (pass ? 'pass' : 'fail')}>{pass ? '✓' : '🚫'}</div>
|
|
{this.props.rightButton && <div className="button right" onClick={this.rightClicked}>{this.props.rightButton}</div>}
|
|
</span>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
interface GardenEditorProps extends React.Props<GardenEditor> {
|
|
onSaveClicked?(garden: Garden): void;
|
|
test?: Koan.ProducedStatement<any>;
|
|
garden?: Garden;
|
|
}
|
|
interface GardenEditorState {
|
|
garden?: Garden;
|
|
pass?: boolean;
|
|
}
|
|
class GardenEditor extends React.Component<GardenEditorProps, {}> {
|
|
state = { garden: this.props.garden };
|
|
|
|
save = () => {
|
|
this.props.onSaveClicked && this.props.onSaveClicked(this.state.garden);
|
|
};
|
|
|
|
render() {
|
|
return <div className="editor">
|
|
<GardenDisplay garden={this.state.garden} test={this.props.test} editable onChanged={g => this.setState({ garden: g }) } />
|
|
<StonePalette />
|
|
<div className="button save" onClick={this.save}>{'💾'}</div>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
class StonePalette extends React.Component<{}, {}> {
|
|
render() {
|
|
let items: JSX.Element[] = [];
|
|
Gardens.RockColors.forEach(color => {
|
|
Gardens.RockShapes.forEach(shape => {
|
|
let name = getGardenName(color, shape);
|
|
let extraProps = { id: name, key: name };
|
|
let index = items.length;
|
|
items.push(<GardenCell
|
|
color={color}
|
|
shape={shape}
|
|
index={index}
|
|
movable
|
|
{...extraProps} />)
|
|
});
|
|
});
|
|
return <div className="palette">{items}</div>;
|
|
}
|
|
}
|
|
|
|
function getGardenName(color: Gardens.RockColor, shape: Gardens.RockShape) {
|
|
return 'draggable.' + Gardens.RockShape[shape] + '.' + Gardens.RockColor[color];
|
|
}
|
|
|
|
|