/// // 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 { 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 { 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 { 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|Array): boolean; } interface ProducedSelector { test(g: Garden, index: number): string|number|boolean; getDescription(plural: DescribeContext): string; seenAllValues(): boolean; } export function buildSelector(spec: SelectorSpec, 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> = []; 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(s: StatementTemplate, args: T): ProducedStatement { let hasPassed = false; let hasFailed = false; let result: ProducedStatement = { 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[] = []; 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(arr: T[]): T { if (arr.length === 0) { return undefined; } else { return arr[Math.floor(Math.random() * arr.length)]; } } function randomWeightedElementOf(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(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; 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 { console.log(list.indexOf(g)); list.splice(list.indexOf(g), 1); renderList(); }} onRightButtonClicked={() => { garden = g; renderEditor(); }} />; } let gardenList =
{list.map(makeGarden)}
; React.render(gardenList, document.getElementById('results')); } let i = 0; function renderEditor() { i++; let editor = { 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 { 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 ; } } interface GardenDisplayProps extends React.Props { garden?: Garden; test?: Koan.ProducedStatement; leftButton?: string; rightButton?: string; onLeftButtonClicked?(): void; onRightButtonClicked?(): void; editable?: boolean; onChanged?(newGarden: Garden): void; } interface GardenDisplayState { garden?: Garden; } class GardenDisplay extends React.Component { 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) => ( { 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
{children}
{this.props.leftButton &&
{this.props.leftButton}
}
{pass ? '✓' : '🚫'}
{this.props.rightButton &&
{this.props.rightButton}
}
; } } interface GardenEditorProps extends React.Props { onSaveClicked?(garden: Garden): void; test?: Koan.ProducedStatement; garden?: Garden; } interface GardenEditorState { garden?: Garden; pass?: boolean; } class GardenEditor extends React.Component { state = { garden: this.props.garden }; save = () => { this.props.onSaveClicked && this.props.onSaveClicked(this.state.garden); }; render() { return
this.setState({ garden: g }) } />
{'💾'}
; } } 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() }); }); return
{items}
; } } function getGardenName(color: Gardens.RockColor, shape: Gardens.RockShape) { return 'draggable.' + Gardens.RockShape[shape] + '.' + Gardens.RockColor[color]; }