';
69 | }
70 |
71 |
72 | /**
73 | * Indicates whether no other Letter in this Letter's string has a descriptor matching the given one.
74 | *
75 | * @param {SlipNode} descriptor - The descriptor to match.
76 | */
77 | isDistinguishingDescriptor(descriptor)
78 | {
79 | let sn = this.wksp.ctx.slipnet;
80 | if ((descriptor == sn.letter) || (descriptor == sn.group) || sn.numbers.includes(descriptor)) {
81 | return false;
82 | }
83 |
84 | if (this.string.objects.some(obj => (obj instanceof Namespace.Letter) && (obj != this) &&
85 | obj.descriptions.some(d => d.descriptor == descriptor))) {
86 | return false;
87 | }
88 |
89 | return true;
90 | }
91 |
92 | };
93 |
94 |
95 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Temperature.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * The Temperature class keeps track of the workspace temperature.
9 | *
10 | */
11 | Namespace.Temperature = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | */
17 | constructor()
18 | {
19 | this.reset();
20 | }
21 |
22 |
23 | /**
24 | * Resets the temperature to 100 and clamps it until time = 30.
25 | *
26 | */
27 | reset()
28 | {
29 | this.actualValue = 100;
30 | this.lastUnclampedValue = 100;
31 | this.clamped = true;
32 | this.clampTime = 30;
33 | }
34 |
35 |
36 | /**
37 | * Returns the current temperature value.
38 | *
39 | */
40 | value() {
41 | return this.clamped ? 100 : this.actualValue;
42 | }
43 |
44 |
45 | /**
46 | * Sets the temperature to the given value, unless we
47 | * are currently clamped, in which case the value is cached.
48 | *
49 | * @param {Number} value - The value to set.
50 | */
51 | set(value)
52 | {
53 | this.lastUnclampedValue = value;
54 | this.actualValue = this.clamped ? 100 : value;
55 | }
56 |
57 |
58 | /**
59 | * Clamps the temperature until the given time.
60 | *
61 | * @param {Number} when - The time to unclamp.
62 | */
63 | clampUntil(when)
64 | {
65 | this.clamped = true;
66 | this.clampTime = when;
67 | }
68 |
69 |
70 | /**
71 | * Unclamps the temperature if the given time is greater than
72 | * the clamping time.
73 | *
74 | * @param {Number} currentTime - The current time.
75 | */
76 |
77 | tryUnclamp(currentTime) {
78 | if (this.clamped && (currentTime >= this.clampTime)) {
79 | this.clamped = false;
80 | }
81 | }
82 |
83 |
84 | /**
85 | * Adjusts the given value according to the current temperature.
86 | * (The value is raised to a power that decreases with temperature.)
87 | *
88 | * @param {Number} input - The value to adjust.
89 | */
90 | getAdjustedValue(input)
91 | {
92 | const exp = (100 - this.value())/30 + 0.5;
93 | return Math.pow(input, exp);
94 | }
95 |
96 |
97 | /**
98 | * Ajusts the given probability value based on the current temperature.
99 | * If the temperature is 0, no adjustment is made. Otherwise, values
100 | * above .5 are lowered and values below .5 are raised.
101 | *
102 | * @param {Number} inProb - The probability to adjust.
103 | */
104 | getAdjustedProb(inProb)
105 | {
106 | if (inProb === 0) { return 0; }
107 | if (inProb === 0.5) { return 0.5; }
108 |
109 | const temp = this.value();
110 | if (inProb < 0.5) {
111 | return 1 - this.getAdjustedProb(1 - inProb);
112 | }
113 | else {
114 | return Math.max(0.5, inProb*(1 - (10 - Math.sqrt(100 - temp))/100) );
115 | }
116 | }
117 |
118 | };
119 |
120 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/TopDownDescriptionScout.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * Thic codelet seeks an appropriate Description for a randomly
9 | * selected workspace object.
10 | *
11 | */
12 | Namespace.Codelets.TopDownDescriptionScout = class extends Namespace.Codelets.CodeletBase
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {Copycat} ctx - The Copycat instance.
18 | * @param {Number} urgency - The urgency of the codelet.
19 | * @param {Array} args - Arguments to pass to the codelet.
20 | * @param {Number} birthdate - The birthdate of the codelet.
21 | */
22 | constructor(ctx, urgency, args, birthdate)
23 | {
24 | super('top-down-description-scout', ctx, urgency, birthdate);
25 | this.descriptionType = args[0]; // e.g., stringPositionCategory, alphabeticPositionCategory
26 | }
27 |
28 |
29 | /**
30 | * Runs the codelet.
31 | */
32 | run()
33 | {
34 | const ctx = this.ctx;
35 | const Utils = Namespace.Codelets.CodeletUtils;
36 |
37 | // Choose a workspace object, based on salience.
38 | const chosenObject = Utils.chooseUnmodifiedObject(ctx, 'totalSalience', ctx.workspace.objects);
39 | if (!chosenObject) { return; }
40 |
41 | // Choose one of the object's descriptions
42 | const descriptions = this._getPossibleDescriptions(chosenObject, this.descriptionType);
43 | if (!descriptions || !descriptions.length) { return; }
44 |
45 | const weights = descriptions.map(d => d.activation);
46 | const chosenDescription = ctx.randGen.weightedChoice(descriptions, weights);
47 |
48 | // Propose the description
49 | ctx.coderack.proposeDescription(chosenObject, chosenDescription.category(), chosenDescription);
50 | }
51 |
52 |
53 | /**
54 | * Gets appropriate descriptions of an object, that match a given description type.
55 | *
56 | * @param {WorkspaceObject} obj - The object to describe
57 | * @param {SlipNode} descriptionType - The description type to test against.
58 | *
59 | * @private
60 | */
61 | _getPossibleDescriptions(obj, descriptionType)
62 | {
63 | const sn = this.ctx.slipnet;
64 | const descriptions = [];
65 | for (let link of descriptionType.instanceLinks) {
66 | const node = link.destination;
67 | if ((node == sn.first) && obj.hasDescriptor(sn.letters[0])) {
68 | descriptions.push(node);
69 | }
70 | else if ((node == sn.last) && obj.hasDescriptor(sn.letters[sn.letters.length-1])) {
71 | descriptions.push(node);
72 | }
73 | else if ((node == sn.middle) && obj.isMiddleObject()) {
74 | descriptions.push(node);
75 | }
76 | for (let i=1; i<=sn.numbers.length; i++) {
77 | if ((node == sn.numbers[i-1]) && (obj instanceof Namespace.Group)) {
78 | if (obj.objectList.length == i) { descriptions.push(node); }
79 | }
80 | }
81 | }
82 | return descriptions;
83 | }
84 |
85 | };
86 |
87 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/Flasher.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for displaying transient animated graphics.
9 | *
10 | */
11 | Namespace.Flasher = class
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {WorkspaceUi} workspaceUi - The parent Ui.
17 | *
18 | */
19 | constructor(workspaceUi)
20 | {
21 | this.wkspUi = workspaceUi;
22 | this.copycat = workspaceUi.copycat;
23 |
24 | // Create my flash canvas
25 | this.canvas = Namespace.UiUtils.CreateElement('canvas',
26 | 'flash-canvas', this.wkspUi.parentDiv, {position:'absolute',
27 | top:'0%', left:'0%', width:'100%', height:'100%',
28 | border: '1px solid', borderTop:0, borderLeft:'none',
29 | zIndex:2}
30 | );
31 |
32 | this.canvasCtx = this.canvas.getContext("2d");
33 | this.flashables = [];
34 | this.state = 'idle';
35 | this.rafId = null;
36 | }
37 |
38 |
39 | /**
40 | * Flashes a graphic a specified number of times.
41 | *
42 | * @param {Graphic} graphic - The graphic to flash.
43 | * @param {Number} flashCount - The number of times to flash.
44 | *
45 | */
46 | flash(graphic, flashCount)
47 | {
48 | if (flashCount <= 0) { return; }
49 | if (this.copycat.stepDelay < 10) { return;} // Don't flash if we're trying to run fast
50 |
51 | this.flashables.push( {graphic:graphic, flashCount:flashCount} );
52 |
53 | if (this.state == 'idle') {
54 | this.state = 'running';
55 | this._animationLoop();
56 | }
57 | }
58 |
59 |
60 | /**
61 | * Indicated whether the animation loop is idle.
62 | *
63 | */
64 | isIdle()
65 | {
66 | return (this.state == 'idle');
67 | }
68 |
69 |
70 | /**
71 | * Runs the animation loop.
72 | *
73 | */
74 | _animationLoop()
75 | {
76 | window.requestAnimationFrame( () =>
77 | {
78 | this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
79 |
80 | // Draw the first-in-line item
81 | const f = this.flashables[0];
82 | f.graphic.redraw(this.canvasCtx);
83 | f.flashCount--;
84 | if (f.flashCount <= 0) {
85 | this.flashables.shift();
86 | }
87 |
88 | // Erase everything after a delay
89 | setTimeout( () => {
90 | window.requestAnimationFrame( () => {
91 | this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
92 |
93 | if (this.flashables.length === 0) {
94 | this.state = 'idle';
95 | this.wkspUi.redraw();
96 | } else {
97 | setTimeout(
98 | () => { this._animationLoop(); },
99 | Math.min(100, 1.5*this.copycat.stepDelay)
100 | );
101 | }
102 | });
103 | }, Math.min(100, 1.5*this.copycat.stepDelay));
104 | });
105 | }
106 |
107 | };
108 |
109 |
110 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/HelpDialog.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 | /**
6 | * @classdesc
7 | * This class implements a dialog for displaying help info.
8 | *
9 | */
10 | Namespace.HelpDialog = class extends Namespace.Dialog
11 | {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {CopycatUi} copycatUi - The parent Ui.
17 | * @param {HTMLElement} parent - The html div that hosts the dialog.
18 | */
19 | constructor(parent)
20 | {
21 | super(parent, 65, 80, 'Copycat Help', false, '#d5bfa2', '#c09f72');
22 | this._buildUi();
23 | }
24 |
25 |
26 | _buildUi()
27 | {
28 | Namespace.UiUtils.StyleElement(this.userDiv, {overflowX:'auto', overflowY:'scroll'});
29 |
30 | this.textDiv = Namespace.UiUtils.CreateElement('div', 'text-div',
31 | this.userDiv, {left:'3%', width:'94%', height:'100%', fontSize:'20px', fontFamily:this.fontFamily}
32 | );
33 |
34 | this.textDiv.innerHTML =
35 | '' +
36 | 'Copycat is a computer model of human analogy-making.
' +
37 | 'It tries to solve letter puzzles of the form "abc is to abd as ijk is to what?"
' +
38 | 'You can enter puzzle strings in the green upper-left area, then click the play button to watch Copycat "think." ' +
39 | 'You can also pause, single-step, reset, and adjust the speed of the demo.
' +
40 |
41 | 'Some interesting examples to try are:
' +
42 | '' +
43 | '- abc → abd, kji → ?
' +
44 | '- abc → abd, iijjkk → ?
' +
45 | '- abc → abd, mrrjjj → ?
' +
46 | '
' +
47 |
48 | 'The algorithm is probabilistic, so you may get different results each time you run it.
' +
49 |
50 | 'While running, Copycat displays
' +
51 | '' +
52 | '- bonds it finds between adjacent letters, as blue lines
' +
53 | '- correspondences it finds between the initial and target strings, as purple jagged lines
' +
54 | '- groups that it thinks may be important, as green boxes
' +
55 | '- descriptions of all the letters, in gray text
' +
56 | '
' +
57 |
58 | 'A flashing graphic indicates a structure that Copycat is considering but hasn’t yet committed to.' +
59 |
60 | 'The thermometer shows how "happy" Copycat is with its current progress; lower temperatures ' +
61 | 'imply greater happiness.
' +
62 |
63 | 'In the yellow Slipnet area, Copycat’s built-in concepts are shown in a grid. ' +
64 | 'The size of the dot over each concept indicates how important that concept is to Copycat at the current moment.
' +
65 |
66 | 'The pink Coderack area displays the list of subroutines or "codelets" that Copycat ' +
67 | 'uses to perform its work. The number of each type of codelet currently in the coderack is shown in a dynamical ' +
68 | 'bar graph. The last-run codelet is indicated by a black triangle.
' +
69 |
70 | 'For (much) more information, check out the book Fluid Concepts and Creative Analogies by Douglas Hofstadter et al., ' +
71 | 'and this app’s repo.
';
72 | }
73 |
74 | };
75 |
76 | })( window.CopycatJS = window.CopycatJS || {} );
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/Codelets/RuleScout.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This codelet tries to propose a Rule based on the descriptions
10 | * of the initial string's changed letter and its replacement.
11 | *
12 | */
13 | Namespace.Codelets.RuleScout = class extends Namespace.Codelets.CodeletBase
14 | {
15 | /**
16 | * @constructor
17 | *
18 | * @param {Copycat} ctx - The Copycat instance.
19 | * @param {Number} urgency - The urgency of the codelet.
20 | * @param {Array} args - Arguments to pass to the codelet. (Empty for this codelet.)
21 | * @param {Number} birthdate - The birthdate of the codelet.
22 | */
23 | constructor(ctx, urgency, args, birthdate)
24 | {
25 | super('rule-scout', ctx, urgency, birthdate);
26 | }
27 |
28 |
29 | /**
30 | * Runs the codelet.
31 | */
32 | run()
33 | {
34 | const ctx = this.ctx;
35 | const sn = ctx.slipnet;
36 | const wksp = ctx.workspace;
37 | const coderack = ctx.coderack;
38 |
39 | // If not all replacements have been found, then fizzle.
40 | const numUnreplaced = wksp.objects.filter(o =>
41 | (o.string == wksp.initialWString) && (o instanceof Namespace.Letter) && !o.replacement).length;
42 | if (numUnreplaced !== 0) { return; }
43 |
44 | const changedObjects = wksp.initialWString.objects.filter(o => o.changed);
45 |
46 | // If there are no changed objects, propose a rule with no changes
47 | if (!changedObjects.length) {
48 | coderack.proposeRule();
49 | return;
50 | }
51 |
52 | // Generate a list of distinguishing descriptions for the first object
53 | let objectList = [];
54 | const changed = changedObjects[changedObjects.length-1];
55 | const position = changed.getDescriptor(sn.stringPositionCategory);
56 | if (position) {
57 | objectList.push(position);
58 | }
59 |
60 | const letter = changed.getDescriptor(sn.letterCategory);
61 | const otherObjectsOfSameLetter = wksp.initialWString.objects.filter(o => (o != changed) && o.getDescriptionType(letter));
62 | if (!otherObjectsOfSameLetter.length) {
63 | objectList.push(letter);
64 | }
65 |
66 | if (changed.correspondence) {
67 | const targetObject = changed.correspondence.objFromTarget;
68 | const newList = [];
69 | const slippages = wksp.getSlippableMappings();
70 | for (let node of objectList) {
71 | node = node.applySlippages(slippages);
72 | if (targetObject.hasDescriptor(node) && targetObject.isDistinguishingDescriptor(node)) {
73 | newList.push(node);
74 | }
75 | }
76 | objectList = newList;
77 | }
78 | if (!objectList.length) { return; }
79 |
80 | // Choose the relation
81 | let weights = objectList.map(o => ctx.temperature.getAdjustedValue(o.depth));
82 | const descriptor = ctx.randGen.weightedChoice(objectList, weights);
83 |
84 | objectList = [];
85 | if (changed.replacement.relation) { objectList.push(changed.replacement.relation); }
86 | objectList.push(changed.replacement.objFromModified.getDescriptor(sn.letterCategory));
87 | weights = objectList.map(o => ctx.temperature.getAdjustedValue(o.depth));
88 | const relation = ctx.randGen.weightedChoice(objectList, weights);
89 |
90 | coderack.proposeRule(sn.letterCategory, descriptor, sn.letter, relation);
91 | }
92 | };
93 |
94 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/WorkspaceString.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class encapsulates an ordered sequence of Letters.
9 | *
10 | */
11 | Namespace.WorkspaceString = class
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {Workspace} wksp - The Workspace instance that will own the string.
17 | * @param {String} jstring - A javascript string to be wrapped.
18 | */
19 | constructor(wksp, jstring)
20 | {
21 | this.wksp = wksp;
22 | this.jstring = jstring || "";
23 | this.letters = [];
24 | this.objects = []; // Letters and Groups
25 | this.bonds = [];
26 | this.intraStringUnhappiness = 0;
27 |
28 | // Create a Letter object for each character in the string
29 | for (let i=0; i descr.build());
39 | }
40 | }
41 |
42 |
43 | /**
44 | * Returns a string describing the object.
45 | *
46 | */
47 | synopsis(type)
48 | {
49 | if (!type) {
50 | return this.jstring;
51 | }
52 | else if (type === 1) {
53 | return '';
54 | }
55 | else {
56 | return this.jstring + ' with ' + this.letters.length.toString() +
57 | ' letters, ' + this.objects.length.toString() + ' objects, ' +
58 | this.bonds.length.toString() + ' bonds.';
59 | }
60 | }
61 |
62 |
63 | /**
64 | * Returns the number of characters in the string.
65 | *
66 | */
67 | get length() {
68 | return this.jstring.length;
69 | }
70 |
71 |
72 | /**
73 | * Updates the relative importances of all objects in the string,
74 | * based on their raw importances and the total number of objects.
75 | *
76 | */
77 | updateRelativeImportances()
78 | {
79 | const total = this.objects.reduce( function(a,b){return a + b.rawImportance;}, 0 );
80 |
81 | if (total === 0) {
82 | for (let obj of this.objects) { obj.relativeImportance = 0; }
83 | }
84 | else {
85 | for (let obj of this.objects) { obj.relativeImportance = obj.rawImportance / total; }
86 | }
87 | }
88 |
89 |
90 | /**
91 | * Sets the string's intraStringUnhappiness value to the
92 | * average intraStringUnhappiness value of its objects.
93 | *
94 | */
95 | updateIntraStringUnhappiness()
96 | {
97 | if (this.objects.length === 0) {
98 | this.intraStringUnhappiness = 0;
99 | }
100 | else {
101 | const total = this.objects.reduce( function(a,b){return a + b.intraStringUnhappiness;}, 0 );
102 | this.intraStringUnhappiness = total / this.objects.length;
103 | }
104 | }
105 |
106 |
107 | /**
108 | * Seeks a Group in the string that matches a given group.
109 | *
110 | * @param {Group} sought - The Group to match.
111 | */
112 | getEquivalentGroup(sought)
113 | {
114 | return this.objects.find(obj =>
115 | (obj instanceof Namespace.Group) && (obj.sameAs(sought))) || null;
116 | }
117 |
118 | };
119 |
120 |
121 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/DescriptionsGraphic.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for drawing the Descriptions of
9 | * letters in a given string.
10 | *
11 | */
12 | Namespace.DescriptionsGraphic = class
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {StringGraphic} stringGraphic - The parent graphic.
18 | *
19 | */
20 | constructor(stringGraphic)
21 | {
22 | this.stringGraphic = stringGraphic;
23 | this.wstring = stringGraphic.wstring;
24 | this.wkspUi = stringGraphic.wkspUi;
25 |
26 | this.descriptionGraphics = stringGraphic.lettersGraphic.letterGraphics.map(
27 | lg => new Namespace.DescriptionGraphic(lg, this));
28 | }
29 |
30 |
31 | /**
32 | * Draws the descriptions.
33 | *
34 | * @param {CanvasRenderingContext2D} ctx - The canvas context
35 | * to draw on.
36 | */
37 | redraw(ctx)
38 | {
39 | this.descriptionGraphics.forEach(g => g.redraw(ctx));
40 | }
41 | };
42 |
43 |
44 | /**
45 | * @classdesc
46 | * This class draws a single Description.
47 | *
48 | */
49 | Namespace.DescriptionGraphic = class
50 | {
51 | /**
52 | * @constructor
53 | *
54 | * @param {LetterGraphic} lg - The associated LetterGraphic.
55 | * @param {DescriptionsGraphic} parent - The DescriptionsGraphic that
56 | * owns this object.
57 | */
58 | constructor(lg, parent)
59 | {
60 | this.letterGraphic = lg;
61 | this.parent = parent;
62 | this.drawParams = {};
63 | }
64 |
65 | /**
66 | * Draws the Description graphics.
67 | *
68 | * @param {CanvasRenderingContext2D} ctx - The canvas context
69 | * to draw on.
70 | */
71 | redraw(ctx)
72 | {
73 | // Update our drawing parameters if necessary
74 | if (Namespace.UiUtils.NeedToRescale(this.drawParams, ctx)) {
75 | this._updateDrawParams(ctx);
76 | }
77 |
78 | const wkspUi = this.parent.wkspUi;
79 | const dp = this.drawParams;
80 | ctx.textAlign = 'left';
81 | ctx.fillStyle = wkspUi.descriptionColor;
82 |
83 | const descrips = this.letterGraphic.letter.descriptions;
84 | for (let i=0; i ctx.fillText(line.text, line.x, line.y));
46 |
47 | ctx.beginPath();
48 | ctx.rect(...dp.rect1);
49 | ctx.stroke();
50 |
51 | ctx.beginPath();
52 | ctx.rect(...dp.rect2);
53 | ctx.stroke();
54 | }
55 | }
56 |
57 |
58 | /**
59 | * Recalculates the drawing parameters for this object.
60 | *
61 | * @private
62 | */
63 | _updateDrawParams(ctx, ruleText)
64 | {
65 | const [w, h] = [ctx.canvas.width, ctx.canvas.height];
66 | const dp = this.drawParams;
67 | dp.canvasWidth = w; dp.canvasHeight = h;
68 |
69 | const targetStringDp = this.wkspUi.targetStringGraphic.drawParams;
70 | const fontSize = Math.max(10, Math.round(targetStringDp.fontSize/2));
71 | dp.font = 'italic ' + fontSize.toString() + 'px serif';
72 |
73 | dp.textLines = [];
74 | if (!ruleText) { return; }
75 |
76 | // Split the text into lines
77 | ctx.font = dp.font;
78 | let lines = [];
79 | let lineCount = 0;
80 | let tmpTxt = ruleText.split(" ");
81 | lines[lineCount] = [];
82 | for (let t = 0; t < tmpTxt.length; t++) {
83 | if (ctx.measureText(lines[lineCount].join(" ")).width > 0.33*w) {
84 | let lastItem = lines[lineCount].pop();
85 | lineCount++;
86 | lines[lineCount] = [lastItem];
87 | }
88 | lines[lineCount].push(tmpTxt[t]);
89 | }
90 | lines = lines.map(line => line.join(" "));
91 | const measures = lines.map(line => ctx.measureText(line));
92 |
93 | // Calculate the text line positions
94 | const xc = this.wkspUi.modifiedStringGraphic.drawParams.stringCenterX;
95 | const y0 = targetStringDp.baselineY + 6*fontSize;
96 | const yStep = 1.3 * fontSize;
97 | dp.textLines = lines.map((line,i) => {
98 | return {text:line, x:xc, y: y0 + i*yStep}; });
99 |
100 | // Calculate the bounding box locations
101 | const maxLineWidth = Math.max(...measures.map(m => m.width));
102 | const left = xc - 0.5*maxLineWidth;
103 | const top = y0 - measures[0].actualBoundingBoxAscent;
104 | const bbox = {x:left, y:top, w:maxLineWidth, h: y0 + yStep*(lines.length-1) - top};
105 |
106 | let inflate = 0.66 * fontSize;
107 | dp.rect1 = [bbox.x - inflate, bbox.y - inflate, bbox.w + 2*inflate, bbox.h + 2*inflate];
108 |
109 | inflate = 1.0 * fontSize;
110 | dp.rect2 = [bbox.x - inflate, bbox.y - inflate, bbox.w + 2*inflate, bbox.h + 2*inflate];
111 | }
112 |
113 | };
114 |
115 |
116 |
117 |
118 |
119 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/BottomUpCorrespondenceScout.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * This codelet looks for potential correspondences between
9 | * objects in the initial and target strings.
10 | *
11 | */
12 | Namespace.Codelets.BottomUpCorrespondenceScout = class extends Namespace.Codelets.CodeletBase
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {Copycat} ctx - The Copycat instance.
18 | * @param {Number} urgency - The urgency of the codelet.
19 | * @param {Array} args - Arguments to pass to the codelet. (Empty for this codelet.)
20 | * @param {Number} birthdate - The birthdate of the codelet.
21 | */
22 | constructor(ctx, urgency, args, birthdate)
23 | {
24 | super('bottom-up-correspondence-scout', ctx, urgency, birthdate);
25 | }
26 |
27 |
28 | /**
29 | * Runs the codelet.
30 | */
31 | run()
32 | {
33 | const ctx = this.ctx;
34 | const wksp = ctx.workspace;
35 | const sn = ctx.slipnet;
36 | const Utils = Namespace.Codelets.CodeletUtils;
37 |
38 | // Choose an object from the initial string, based on salience.
39 | const objFromInitial = Utils.chooseUnmodifiedObject(ctx, 'interStringSalience', wksp.initialWString.objects);
40 | if (!objFromInitial) { return; }
41 |
42 | // Choose an object from the target string, based on salience.
43 | let objFromTarget = Utils.chooseUnmodifiedObject(ctx, 'interStringSalience', wksp.targetWString.objects);
44 | if (!objFromTarget) { return; }
45 |
46 | // Check that the initial and target objects are compatible.
47 | if (objFromInitial.spansString() != objFromTarget.spansString()) { return; }
48 |
49 | // Provide UI feedback.
50 | if (ctx.ui && !ctx.batchMode) {
51 | const dummyCorresp = new Namespace.Correspondence(objFromInitial, objFromTarget, [], false);
52 | ctx.ui.workspaceUi.corrsGraphic.flashGrope(dummyCorresp);
53 | }
54 |
55 | // Get concept mappings between the two objects.
56 | let conceptMappings = Namespace.ConceptMapping.getMappings(objFromInitial, objFromTarget,
57 | objFromInitial.relevantDescriptions(), objFromTarget.relevantDescriptions());
58 | if (!conceptMappings.length) { return; }
59 |
60 | // Check for slippability
61 | const slippageProbs = conceptMappings.map(m => ctx.temperature.getAdjustedProb( m.slippability()/100 ));
62 | const slippable = slippageProbs.some(p => ctx.randGen.coinFlip(p));
63 | if (!slippable) { return; }
64 |
65 | // Get any distinguishing mappings
66 | const distinguishingMappings = conceptMappings.filter( m => m.isDistinguishing() );
67 | if (!distinguishingMappings.length) { return; }
68 |
69 | // If both objects span the strings, then check to see if the string description needs to be flipped.
70 | let flipTargetObject = false;
71 | if (objFromInitial.spansString() && objFromTarget.spansString() && (sn.opposite.activation != 100))
72 | {
73 | const opposites = distinguishingMappings.filter(m =>
74 | (m.initialDescType == sn.stringPositionCategory) && (m.initialDescType != sn.bondFacet));
75 |
76 | if (opposites.every(m => m.label == sn.opposite)) {
77 | const initialDescTypes = opposites.map(m => m.initialDescType);
78 | if (initialDescTypes.includes(sn.directionCategory)) {
79 | objFromTarget = objFromTarget.flippedVersion();
80 | conceptMappings = Namespace.ConceptMapping.getMappings(objFromInitial, objFromTarget,
81 | objFromInitial.relevantDescriptions(), objFromTarget.relevantDescriptions());
82 | flipTargetObject = true;
83 | }
84 | }
85 | }
86 |
87 | // Propose a correspondence.
88 | ctx.coderack.proposeCorrespondence(objFromInitial, objFromTarget, conceptMappings, flipTargetObject);
89 | }
90 |
91 |
92 | };
93 |
94 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/GroupScout_WholeString.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * This codelet tries to make a group out of the entire string.
9 | *
10 | */
11 | Namespace.Codelets.GroupScout_WholeString = class extends Namespace.Codelets.CodeletBase
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {Copycat} ctx - The Copycat instance.
17 | * @param {Number} urgency - The urgency of the codelet.
18 | * @param {Array} args - Arguments to pass to the codelet.
19 | * @param {Number} birthdate - The birthdate of the codelet.
20 | */
21 | constructor(ctx, urgency, args, birthdate)
22 | {
23 | super('group-scout--whole-string', ctx, urgency, birthdate);
24 | this.direction = args[0];
25 | }
26 |
27 |
28 | /**
29 | * Runs the codelet.
30 | */
31 | run()
32 | {
33 | const ctx = this.ctx;
34 | const sn = ctx.slipnet;
35 | const wksp = ctx.workspace;
36 |
37 | // Choose either the initial or target string
38 | const string = ctx.randGen.choice([wksp.initialWString, wksp.targetWString]);
39 |
40 | // Find leftmost object & the highest group to which it belongs
41 | const lms = string.objects.filter(o => o.leftmost);
42 | let leftmost = lms.length ? lms[0] : null;
43 | if (!leftmost) { return; }
44 | while (leftmost.group && (leftmost.group.bondCategory == sn.sameness)) {
45 | leftmost = leftmost.group;
46 | }
47 |
48 | if (leftmost.spansString()) {
49 | // The object already spans the string - propose this object
50 | if (leftmost instanceof Namespace.Group) {
51 | const lmGroup = leftmost;
52 | ctx.coderack.proposeGroup(lmGroup.objectList, lmGroup.bondList,
53 | lmGroup.groupCategory, lmGroup.directionCategory, lmGroup.facet);
54 | }
55 | else {
56 | ctx.coderack.proposeGroup([leftmost], [], sn.samenessGroup, null, sn.letterCategory);
57 | }
58 | return;
59 | }
60 |
61 | let bonds = [];
62 | const objects = [leftmost];
63 | while (leftmost.rightBond) {
64 | bonds.push(leftmost.rightBond);
65 | leftmost = leftmost.rightBond.rightObject;
66 | objects.push(leftmost);
67 | }
68 | if (!leftmost.rightmost) { return; }
69 |
70 | // Choose a random bond from list
71 | const chosenBond = ctx.randGen.choice(bonds);
72 | bonds = this._possibleGroupBonds(chosenBond, bonds);
73 | if (!bonds.length) { return; }
74 |
75 | const groupCategory = chosenBond.category.getRelatedNode(sn.groupCategory);
76 | ctx.coderack.proposeGroup(objects, bonds, groupCategory, chosenBond.directionCategory, chosenBond.facet);
77 | }
78 |
79 |
80 | /**
81 | * From a given list of bonds, get bonds that match the chosen bond.
82 | * @private
83 | */
84 | _possibleGroupBonds(chosenBond, bonds)
85 | {
86 | const result = [];
87 |
88 | for (let bond of bonds) {
89 | if ((bond.category == chosenBond.category) &&
90 | (bond.directionCategory == chosenBond.directionCategory)) {
91 | result.push(bond);
92 | }
93 | else {
94 | // A modified bond might be made
95 | if (bond.category == chosenBond.category) {
96 | return [];
97 | }
98 | if (bond.directionCategory == chosenBond.directionCategory) {
99 | return [];
100 | }
101 | if ([chosenBond.category, bond.category].includes(this.ctx.slipnet.sameness)) {
102 | return [];
103 | }
104 | const newBond = new Namespace.Bond(bond.destination, bond.source, chosenBond.category,
105 | chosenBond.facet, bond.destDescriptor, bond.sourceDescriptor);
106 | result.push(newBond);
107 | }
108 | }
109 | return result;
110 | }
111 |
112 | };
113 |
114 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/CopycatUi.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This is the top level container for the Copycat UI elements.
9 | *
10 | */
11 | Namespace.CopycatUi = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {Copycat} copycat - The Copycat instance to be visualized.
17 | */
18 | constructor(copycat)
19 | {
20 | this.copycat = copycat;
21 | this.rafId = null;
22 |
23 | // Create the UI elements
24 | this.topbarUi = new Namespace.TopbarUi(this, document.getElementById('topbar_area'));
25 | this.inputUi = new Namespace.InputUi(this, document.getElementById('input_area'));
26 | this.workspaceHeaderUi = new Namespace.WorkspaceHeaderUi(this, document.getElementById('workspace_header_area'));
27 | this.workspaceUi = new Namespace.WorkspaceUi(this, document.getElementById('workspace_area'));
28 | this.slipnetUi = new Namespace.SlipnetUi(this, document.getElementById('slipnet_area'));
29 | this.coderackUi = new Namespace.CoderackUi(this, document.getElementById('coderack_area'));
30 | this.batchmodeUi = new Namespace.BatchmodeUi(this, document.getElementById('batchmode_area'));
31 |
32 | this.singleModeUiList = [this.topbarUi, this.inputUi, this.workspaceHeaderUi, this.workspaceUi, this.slipnetUi, this.coderackUi];
33 | this.batchModeUiList = [this.topbarUi, this.inputUi, this.batchmodeUi];
34 | this.uiList = this.singleModeUiList;
35 |
36 | // Listen for resize events
37 | const resizeFunc = this._onResize.bind(this);
38 | window.addEventListener('resize', resizeFunc);
39 | window.addEventListener('orientationchange', resizeFunc);
40 |
41 | // Do an initial rendering, after the DOM settles.
42 | setTimeout( resizeFunc, 250 );
43 | setTimeout( resizeFunc, 1500 );
44 | }
45 |
46 |
47 | /**
48 | * Handler for state-change events
49 | * @private
50 | *
51 | */
52 | _onCopycatStateChange()
53 | {
54 | this.uiList.forEach( ui => ui._onCopycatStateChange() );
55 | }
56 |
57 |
58 | /**
59 | * Handler for batchmode-toggle events
60 | * @private
61 | *
62 | */
63 | _onBatchModeToggled()
64 | {
65 | this.batchmodeUi.parentDiv.style.display = this.copycat.batchMode ? 'block' : 'none';
66 |
67 | [this.workspaceHeaderUi, this.workspaceUi, this.slipnetUi, this.coderackUi].forEach(
68 | ui => ui.parentDiv.style.display = this.copycat.batchMode ? 'none' : 'block');
69 |
70 | this.inputUi.parentDiv.style.width = this.copycat.batchMode ? '100%' : '60%';
71 |
72 | this.uiList = this.copycat.batchMode ? this.batchModeUiList : this.singleModeUiList;
73 | this._onCopycatStateChange();
74 | this._onResize();
75 | }
76 |
77 |
78 | /**
79 | * Handler for resize events.
80 | * @private
81 | *
82 | */
83 | _onResize()
84 | {
85 | if ( this.rafId ) { window.cancelAnimationFrame(this.rafId); }
86 |
87 | this.rafId = window.requestAnimationFrame( () => {
88 | this.uiList.forEach( ui => ui._onResize() );
89 | this.rafId = null;
90 | }
91 | );
92 | }
93 |
94 |
95 | /**
96 | * Makes sure that all input strings are valid.
97 | * @private
98 | *
99 | */
100 | checkInputStrings()
101 | {
102 | const wksp = this.copycat.workspace;
103 | const inputStrings = this.inputUi.getInputStrings();
104 | const wkspStrings = [wksp.initialWString.jstring, wksp.modifiedWString.jstring, wksp.targetWString.jstring];
105 | const inputModified = !inputStrings.every((str, idx) => str.toLowerCase() == wkspStrings[idx]);
106 |
107 | if (inputModified) {
108 | if (inputStrings.every(this.copycat.checkInputString)) {
109 | this.copycat.setStrings(...inputStrings);
110 | return true;
111 | } else {
112 | this.inputUi.displayMessage('Invalid input!');
113 | return false;
114 | }
115 | }
116 | else {
117 | return true;
118 | }
119 | }
120 |
121 | };
122 |
123 |
124 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/TopbarUi.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class draws the title bar at the top of the screen.
9 | *
10 | */
11 | Namespace.TopbarUi = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {CopycatUi} copycatUi - The parent Ui.
17 | * @param {HTMLElement} parentDiv - The html div that hosts
18 | * this ui.
19 | */
20 | constructor(copycatUi, parentDiv)
21 | {
22 | this.copycatUi = copycatUi;
23 | this.parentDiv = parentDiv;
24 | this.copycat = copycatUi.copycat;
25 | this.drawParams = {};
26 |
27 | this._buildUi(parentDiv);
28 | }
29 |
30 |
31 | /**
32 | * Creates the ui elements.
33 | * @private
34 | *
35 | */
36 | _buildUi()
37 | {
38 | const UiUtils = Namespace.UiUtils;
39 |
40 | this.mainDiv = UiUtils.CreateElement('div',
41 | 'topbar-div', this.parentDiv,
42 | {position:'absolute', top:'0%', left:'0%', width:'100%',
43 | height:'100%', background:'#bfcbdf'}
44 | );
45 |
46 | this.logoImg = UiUtils.CreateElement('img', 'logo', this.mainDiv,
47 | {width:'auto', top:0, height:'100%', left:'1vh'},
48 | {src:'./cc_logo.png'}
49 | );
50 | this.logoImg.className += " noselect";
51 |
52 | this.titleSpan = UiUtils.CreateElement('span', 'title-span',
53 | this.mainDiv, {top:'0%', height:'100%', left:'10vh', width:'auto',
54 | display:'flex', alignItems:'center', justifyContent:'left',
55 | color:'#404040', fontFamily:'Arial', fontWeight:'bold',
56 | fontStyle:'italic', fontSize: '4.25vh'}
57 | );
58 | this.titleSpan.innerHTML = 'Copycat';
59 | this.titleSpan.className += " noselect";
60 |
61 | this.helpBtn = UiUtils.CreateElement('button', 'help-btn',
62 | this.mainDiv, {top:'22%', height:'56%', right:'2vh', width:'10vh',
63 | display:'flex', alignItems:'center', justifyContent:'center',
64 | color:'#404040', fontFamily:'Arial', fontWeight:'normal',
65 | fontSize: '3vh', background:'#dfdfdf', border:'1px solid #404040' }
66 | );
67 | this.helpBtn.innerHTML = ' Help ';
68 | this.helpBtn.className += " noselect";
69 | this.helpBtn.onclick = this._onHelpBtnClick.bind(this);
70 |
71 | this.batchModeBtn = UiUtils.CreateElement('button', 'batchmode-btn',
72 | this.mainDiv, {top:'22%', height:'56%', right:'15vh', width:'auto',
73 | display:'flex', alignItems:'center', justifyContent:'center',
74 | color:'#404040', fontFamily:'Arial', fontWeight:'normal',
75 | fontSize: '1.4vh', background:'#dfdfdf', border:'1px solid #404040' }
76 | );
77 | this.batchModeBtn.innerHTML = ' Toggle
Batch Mode ';
78 | this.batchModeBtn.className += " noselect";
79 | this.batchModeBtn.onclick = this._onBatchModeBtnClick.bind(this);
80 |
81 | this.helpDialog = new Namespace.HelpDialog(document.getElementById('app_area'));
82 | }
83 |
84 |
85 | /**
86 | * Handler for state change events.
87 | * @private
88 | *
89 | */
90 | _onCopycatStateChange()
91 | {
92 | const running = (this.copycat.state == 'running');
93 | this.batchModeBtn.disabled = running;
94 | this.batchModeBtn.style.opacity = running ? '0.4' : '1.0';
95 | }
96 |
97 |
98 | /**
99 | * Handler for resize events.
100 | * @private
101 | *
102 | */
103 | _onResize()
104 | {
105 | // Nothing to do here
106 | }
107 |
108 |
109 | /**
110 | * Handler for help-button clicks.
111 | * @private
112 | *
113 | */
114 | _onHelpBtnClick()
115 | {
116 | if (this.helpDialog.isShown()) {
117 | this.helpDialog.hide();
118 | }
119 | else {
120 | this.helpDialog.show();
121 | }
122 | }
123 |
124 |
125 | /**
126 | * Handler for batchmode-button clicks.
127 | * @private
128 | *
129 | */
130 | _onBatchModeBtnClick()
131 | {
132 | this.copycat.toggleBatchMode(!this.copycat.batchMode);
133 | }
134 |
135 | };
136 |
137 |
138 | })( window.CopycatJS = window.CopycatJS || {} );
139 |
--------------------------------------------------------------------------------
/src/Codelets/TopDownGroupScout_Direction.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * This codelet seeks potential Groups in the initial and target strings.
9 | *
10 | */
11 | Namespace.Codelets.TopDownGroupScout_Direction = class extends Namespace.Codelets.CodeletBase
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {Copycat} ctx - The Copycat instance.
17 | * @param {Number} urgency - The urgency of the codelet.
18 | * @param {Array} args - Arguments to pass to the codelet.
19 | * @param {Number} birthdate - The birthdate of the codelet.
20 | */
21 | constructor(ctx, urgency, args, birthdate)
22 | {
23 | super('top-down-group-scout--direction', ctx, urgency, birthdate);
24 | this.direction = args[0];
25 | }
26 |
27 |
28 | /**
29 | * Runs the codelet.
30 | */
31 | run()
32 | {
33 | const ctx = this.ctx;
34 | const sn = ctx.slipnet;
35 | const CodeletUtils = Namespace.Codelets.CodeletUtils;
36 |
37 | let direction = this.direction;
38 | let source = CodeletUtils.getScoutSource(ctx, 'bondDirection', direction);
39 | if (!source || source.spansString()) { return; }
40 |
41 | const myDir = source.leftmost ? sn.right : source.rightmost ? sn.left :
42 | ctx.randGen.weightedChoice([sn.left, sn.right], [sn.left.activation, sn.right.activation]);
43 |
44 | let firstBond = (myDir == sn.left) ? source.leftBond : source.rightBond;
45 | if (firstBond && !firstBond.directionCategory) {
46 | direction = null;
47 | }
48 |
49 | if (!firstBond || (firstBond.directionCategory != direction)) {
50 | firstBond = (myDir == sn.right) ? source.leftBond : source.rightBond;
51 | if (firstBond && !firstBond.directionCategory) {
52 | direction = null;
53 | }
54 | if (!firstBond || (firstBond.directionCategory != direction)) { return; }
55 | }
56 |
57 | const category = firstBond.category;
58 | if (!category) { return; }
59 |
60 | const groupCategory = category.getRelatedNode(sn.groupCategory);
61 | let bondFacet = null;
62 | // Find the leftmost object in group with these bonds
63 | let search = true;
64 | while (search) {
65 | search = false;
66 | if (!source.leftBond) {
67 | continue;
68 | }
69 | if (source.leftBond.category != category) {
70 | continue;
71 | }
72 | if (source.leftBond.directionCategory != direction) {
73 | if (source.leftBond.directionCategory) {
74 | continue;
75 | }
76 | }
77 | if (!bondFacet || (bondFacet == source.leftBond.facet)) {
78 | bondFacet = source.leftBond.facet;
79 | direction = source.leftBond.directionCategory;
80 | source = source.leftBond.leftObject;
81 | search = true;
82 | }
83 | }
84 |
85 | // Find rightmost object in group with these bonds
86 | search = true;
87 | let destination = source;
88 | while (search) {
89 | search = false;
90 | if (!destination.rightBond) {
91 | continue;
92 | }
93 | if (destination.rightBond.category != category) {
94 | continue;
95 | }
96 | if (destination.rightBond.directionCategory != direction) {
97 | if (destination.rightBond.directionCategory) {
98 | continue;
99 | }
100 | }
101 | if (!bondFacet || (bondFacet == destination.rightBond.facet)) {
102 | bondFacet = destination.rightBond.facet;
103 | direction = source.rightBond.directionCategory;
104 | destination = destination.rightBond.rightObject;
105 | search = true;
106 | }
107 | }
108 | if (destination == source) { return; }
109 |
110 | const objects = [source];
111 | const bonds = [];
112 | while (source != destination) {
113 | bonds.push(source.rightBond);
114 | objects.push(source.rightBond.rightObject);
115 | source = source.rightBond.rightObject;
116 | }
117 | ctx.coderack.proposeGroup(objects, bonds, groupCategory, direction, bondFacet);
118 |
119 | }
120 | };
121 |
122 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/ImportantObjectCorrespondenceScout.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This codelet chooses an object and its description from the initial string,
10 | * and looks for an object in the target string with the same description or
11 | * slipped description. It then tries to propose a Correspondence between
12 | * the two objects.
13 | */
14 | Namespace.Codelets.ImportantObjectCorrespondenceScout = class extends Namespace.Codelets.CodeletBase
15 | {
16 | /**
17 | * @constructor
18 | *
19 | * @param {Copycat} ctx - The Copycat instance.
20 | * @param {Number} urgency - The urgency of the codelet.
21 | * @param {Array} args - Arguments to pass to the codelet.
22 | * @param {Number} birthdate - The birthdate of the codelet.
23 | */
24 | constructor(ctx, urgency, args, birthdate)
25 | {
26 | super('important-object-correspondence-scout', ctx, urgency, birthdate);
27 | }
28 |
29 |
30 | /**
31 | * Runs the codelet.
32 | */
33 | run()
34 | {
35 | const ctx = this.ctx;
36 | const wksp = ctx.workspace;
37 | const sn = ctx.slipnet;
38 | const Utils = Namespace.Codelets.CodeletUtils;
39 |
40 | // Choose an object from the initial string, based on salience.
41 | const objFromInitial = Utils.chooseUnmodifiedObject(ctx, 'relativeImportance', wksp.initialWString.objects);
42 | if (!objFromInitial) { return; }
43 |
44 | const descriptors = objFromInitial.relevantDistinguishingDescriptors();
45 | const weights = descriptors.map( d => ctx.temperature.getAdjustedValue(d.depth) );
46 | const descriptor = ctx.randGen.weightedChoice(descriptors, weights);
47 | if (!descriptor) { return; }
48 |
49 | let initialDescriptor = descriptor;
50 | for (let m of wksp.getSlippableMappings()) {
51 | if (m.initialDescriptor == descriptor) {
52 | initialDescriptor = m.targetDescriptor;
53 | break;
54 | }
55 | }
56 | const targetCandidates = wksp.targetWString.objects.filter(
57 | o => o.relevantDescriptions().some(d => d.descriptor == initialDescriptor));
58 | if (!targetCandidates.length) { return; }
59 |
60 | let objFromTarget = Utils.chooseUnmodifiedObject(ctx, 'interStringSalience', targetCandidates);
61 | if (objFromInitial.spansString() != objFromTarget.spansString()) { return; }
62 |
63 | // Provide UI feedback
64 | if (ctx.ui && !ctx.batchMode) {
65 | const dummyCorresp = new Namespace.Correspondence(
66 | objFromInitial, objFromTarget, [], false);
67 | ctx.ui.workspaceUi.corrsGraphic.flashGrope(dummyCorresp);
68 | }
69 |
70 | // Get the posible concept mappings
71 | let conceptMappings = Namespace.ConceptMapping.getMappings(
72 | objFromInitial, objFromTarget, objFromInitial.relevantDescriptions(),
73 | objFromTarget.relevantDescriptions());
74 |
75 | // Check for slippability
76 | const slippageProbs = conceptMappings.map(
77 | m => ctx.temperature.getAdjustedProb( m.slippability()/100 ));
78 | const slippable = slippageProbs.some(p => ctx.randGen.coinFlip(p));
79 | if (!slippable) { return; }
80 |
81 | // Find out if any are distinguishing
82 | const distinguishingMappings = conceptMappings.filter(m => m.isDistinguishing());
83 | if (!distinguishingMappings.length) { return; }
84 |
85 | // If both objects span the strings, check to see if the
86 | // string description needs to be flipped
87 | const opposites = distinguishingMappings.filter(m =>
88 | (m.initialDescType == sn.stringPositionCategory) && (m.initialDescType != sn.bondFacet));
89 | const initialDescriptionTypes = opposites.map(m => m.initialDescType);
90 | let flipTargetObject = false;
91 | if (objFromInitial.spansString() && objFromTarget.spansString() && (sn.opposite.activation != 100) &&
92 | initialDescriptionTypes.includes(sn.directionCategory) && opposites.every(m => m.label == sn.opposite) ) {
93 | objFromTarget = objFromTarget.flippedVersion();
94 | conceptMappings = Namespace.ConceptMapping.getMappings( objFromInitial, objFromTarget,
95 | objFromInitial.relevantDescriptions(), objFromTarget.relevantDescriptions() );
96 | flipTargetObject = true;
97 | }
98 |
99 | ctx.coderack.proposeCorrespondence(objFromInitial, objFromTarget, conceptMappings, flipTargetObject);
100 | }
101 |
102 |
103 | };
104 |
105 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Description.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class encapsulates a description of a Letter or Group.
9 | *
10 | * A description consists of a (descriptionType, descriptor) pair. Some examples are:
11 | *
12 | * - (objectCategory, letter)
13 | * - (letterCategory, a)
14 | * - (stringPositionCategory, leftmost)
15 | * - (groupCategory, samenessGroup)
16 | *
17 | *
18 | */
19 | Namespace.Description = class {
20 |
21 | /**
22 | * @constructor
23 | *
24 | * @param {WorkspaceObject} obj - The object being described.
25 | * @param {SlipNode} descriptionType - The aspect being described, e.g., objectCategory.
26 | * @param {SlipNode} descriptor - The value of the aspect, e.g., letter.
27 | */
28 | constructor(obj, descriptionType, descriptor)
29 | {
30 | // WorkspaceStructure members
31 | this.wksp = obj.wksp;
32 | this.string = obj.string;
33 | this.totalStrength = 0;
34 |
35 | // Description members
36 | this.object = obj;
37 | this.descriptionType = descriptionType;
38 | this.descriptor = descriptor;
39 | }
40 |
41 |
42 | /**
43 | * Returns a string describing the object.
44 | */
45 | synopsis(type)
46 | {
47 | const wksp = this.wksp;
48 | let s = this.object.synopsis(1);
49 |
50 | if (this.object.string == wksp.initialWString) {
51 | s += ' in initial string';
52 | }
53 | else if (this.object.string == wksp.modifiedWString) {
54 | s += ' in modified string';
55 | }
56 | else if (this.object.string == wksp.targetWString) {
57 | s += ' in target string';
58 | }
59 | else {
60 | s += ' in unknown string';
61 | }
62 | s += ', ' + this.descriptionType.name + ' == ' + this.descriptor.name;
63 |
64 | return !type ? s : '';
65 | }
66 |
67 |
68 | /**
69 | * Indicates whether this Description has the same descriptionType and
70 | * descriptor as another one.
71 | *
72 | * @param {Description} other - The Description to compare with.
73 | */
74 | sameAs(other)
75 | {
76 | return ((other.descriptionType == this.descriptionType) && (other.descriptor == this.descriptor));
77 | }
78 |
79 |
80 | /**
81 | * Sets the activation of the descriptionType and descriptor to 100.
82 | *
83 | */
84 | activate()
85 | {
86 | this.descriptionType.activation = 100;
87 | this.descriptor.activation = 100;
88 | }
89 |
90 |
91 | /**
92 | * Updates the total strength value.
93 | *
94 | */
95 | updateStrength()
96 | {
97 | // Internal strength
98 | let internalStrength = this.descriptor.depth;
99 |
100 | // Local support: Count the number of other objects in this
101 | // object's string that are described like this one.
102 | let numDescribedLikeThis = 0;
103 | for (let other of this.string.objects.filter(o => o != this.object)) {
104 | if ( !this.object.isWithin(other) && !other.isWithin(this.object) ) {
105 | numDescribedLikeThis += other.descriptions.filter(od => od.descriptionType == this.descriptionType).length;
106 | }
107 | }
108 | const supportVals = [0, 20, 60, 90, 100];
109 | const localSupport = supportVals[Math.min(numDescribedLikeThis,4)];
110 |
111 | // External strength
112 | let externalStrength = (localSupport + this.descriptionType.activation)/2;
113 |
114 | // Total strength
115 | const wti = internalStrength / 100;
116 | const wte = 1 - wti;
117 | this.totalStrength = wti*internalStrength + wte*externalStrength;
118 | }
119 |
120 |
121 | /**
122 | * Activates the description and adds it to its owner's description list
123 | * and to the workspace.
124 | */
125 | build()
126 | {
127 | this.activate();
128 | if (!this.object.hasDescriptor(this.descriptor)) {
129 | this.object.descriptions.push(this);
130 | }
131 | if (!this.wksp.structures.includes(this)) {
132 | this.wksp.structures.push(this);
133 | }
134 | }
135 |
136 |
137 | /**
138 | * Removes the Description from its owner's description list, and
139 | * from the workspace structures list.
140 | */
141 | break()
142 | {
143 | this.wksp.structures = this.wksp.structures.filter(s => s !== this);
144 | this.object.descriptions = this.object.descriptions.filter(s => s !== this);
145 | }
146 |
147 | };
148 |
149 |
150 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/WorkspaceUi.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for displaying the state of the Workspace.
9 | *
10 | */
11 | Namespace.WorkspaceUi = class
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {CopycatUi} copycatUi - The parent Ui.
17 | * @param {HTMLElement} parentDiv - The html div that hosts this ui.
18 | */
19 | constructor(copycatUi, parentDiv)
20 | {
21 | this.copycatUi = copycatUi;
22 | this.copycat = copycatUi.copycat;
23 | this.workspace = this.copycat.workspace;
24 | this.parentDiv = parentDiv;
25 |
26 | // Specify colors
27 | this.groupColor = '#00ff00';
28 | this.bondColor = '#1581e7';
29 | this.correspColor = '#a020f0';
30 | this.letterColor = '#000000';
31 | this.answerLetterColor = '#d20000';
32 | this.replColor = '#964B00';
33 | this.ruleColor = '#d20000';
34 | this.descriptionColor = '#c0c0c0';
35 | this.activeDescriptionColor = '#909090';
36 |
37 | // Create my canvas
38 | this.canvas = Namespace.UiUtils.CreateElement('canvas',
39 | 'wksp-canvas', parentDiv, {position:'absolute',
40 | top:'0%', left:'0%', width:'100%', height:'100%',
41 | border: '1px solid', borderTop:0, borderLeft:'none',
42 | background:'#fafafa', zIndex:1}
43 | );
44 |
45 | // Create my flasher
46 | this.flasher = new Namespace.Flasher(this);
47 |
48 | // Create my graphics objects
49 | this.initialStringGraphic = new Namespace.StringGraphic(this, 0);
50 | this.modifiedStringGraphic = new Namespace.StringGraphic(this, 1);
51 | this.answerStringGraphic = new Namespace.StringGraphic(this, 2);
52 | this.targetStringGraphic = new Namespace.StringGraphic(this, 3);
53 | this.arrowGraphic = new Namespace.ArrowGraphic(this);
54 | this.ruleGraphic = new Namespace.RuleGraphic(this);
55 | this.replacementsGraphic = new Namespace.ReplacementsGraphic(this);
56 | this.corrsGraphic = new Namespace.CorrsGraphic(this);
57 |
58 | this.allGraphics = [
59 | this.initialStringGraphic,
60 | this.modifiedStringGraphic,
61 | this.targetStringGraphic,
62 | this.answerStringGraphic,
63 | this.arrowGraphic,
64 | this.ruleGraphic,
65 | this.replacementsGraphic,
66 | this.corrsGraphic
67 | ];
68 | }
69 |
70 |
71 | /**
72 | * Gets the StringGraphic object for a specified string.
73 | *
74 | * @param {WorkspaceString} wstring - The string to search on.
75 | */
76 | getStringGraphic(wstring)
77 | {
78 | const wksp = this.workspace;
79 | return (wstring === wksp.initialWString) ? this.initialStringGraphic :
80 | (wstring === wksp.targetWString) ? this.targetStringGraphic :
81 | (wstring === wksp.modifiedWString) ? this.modifiedStringGraphic :
82 | (wstring === wksp.answerWString) ? this.answerStringGraphic :
83 | null;
84 | }
85 |
86 |
87 | /**
88 | * Handler for state-change events
89 | * @private
90 | *
91 | */
92 | _onCopycatStateChange()
93 | {
94 | this.redraw();
95 | }
96 |
97 |
98 | /**
99 | * Handler for resize events.
100 | * @private
101 | *
102 | */
103 | _onResize()
104 | {
105 | this.redraw();
106 | }
107 |
108 |
109 | /**
110 | * Updates the UI.
111 | *
112 | */
113 | redraw()
114 | {
115 | const UiUtils = Namespace.UiUtils;
116 | const canvas = this.canvas;
117 | const ctx = canvas.getContext("2d");
118 |
119 | // Resize the canvas if necessary
120 | if ( !UiUtils.RightsizeCanvas(canvas) || !UiUtils.RightsizeCanvas(this.flasher.canvas)) { return; }
121 |
122 | // Bounce out if the input strings haven't been set
123 | if (!this.copycat.workspace.initialWString) { return; }
124 |
125 | // Avoid redrawing while flashing
126 | if ( !this.flasher.isIdle() ) { return; }
127 |
128 | // Re-draw all the graphics
129 | ctx.clearRect(0, 0, canvas.width, canvas.height);
130 | this.allGraphics.forEach( g => g.redraw(ctx) );
131 | }
132 |
133 |
134 | /**
135 | * Flashes the specified graphic item.
136 | *
137 | * @param {Graphic} item - The item to flash.
138 | * @param {Number} flashCount - The number of times to flash.
139 | *
140 | */
141 | flash(graphic, flashCount)
142 | {
143 | this.flasher.flash(graphic, flashCount);
144 | }
145 |
146 | };
147 |
148 |
149 | })( window.CopycatJS = window.CopycatJS || {} );
150 |
--------------------------------------------------------------------------------
/src/UI/ReplacementsGraphic.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for drawing Replacement lines.
9 | *
10 | */
11 | Namespace.ReplacementsGraphic = class
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {WorkspaceUi} workspaceUi - The parent UI.
17 | *
18 | */
19 | constructor(workspaceUi)
20 | {
21 | this.wkspUi = workspaceUi;
22 | this.initialString = workspaceUi.initialStringGraphic.wstring;
23 | this.modifiedString = workspaceUi.modifiedStringGraphic.wstring;
24 | this.cache = [];
25 | }
26 |
27 |
28 | /**
29 | * Draws the replacement lines.
30 | *
31 | * @param {CanvasRenderingContext2D} ctx - The canvas context
32 | * to draw on.
33 | */
34 | redraw(ctx)
35 | {
36 | // Check whether the input strings have changed
37 | const wkspUi = this.wkspUi;
38 | if ((this.initialString !== wkspUi.initialStringGraphic.wstring) ||
39 | (this.modifiedString !== wkspUi.modifiedStringGraphic.wstring)) {
40 | this.initialString = wkspUi.initialStringGraphic.wstring;
41 | this.modifiedString = wkspUi.modifiedStringGraphic.wstring;
42 | this.cache = [];
43 | }
44 |
45 | // Draw all the replacements that have been found so far
46 | const wksp = this.wkspUi.workspace;
47 | const replacements = wksp.initialWString.letters.map(ltr => ltr.replacement).filter(repl => !!repl);
48 | replacements.forEach(r => { this._getReplacementGraphic(r).redraw(ctx); });
49 | }
50 |
51 |
52 | /**
53 | * Gets or creates the graphic for a given Replacement object.
54 | * @private
55 | */
56 | _getReplacementGraphic(repl)
57 | {
58 | let replGraphic = this.cache.find(g => g.replacement == repl);
59 |
60 | if (!replGraphic) {
61 | replGraphic = new Namespace.ReplacementGraphic(repl, this);
62 | this.cache.push(replGraphic);
63 | if (this.cache.length > 25) {
64 | this.cache.shift();
65 | }
66 | }
67 | return replGraphic;
68 | }
69 | };
70 |
71 |
72 | /**
73 | * @classdesc
74 | * This class is responsible for drawing a single Replacement line.
75 | *
76 | */
77 | Namespace.ReplacementGraphic = class
78 | {
79 | /**
80 | * @constructor
81 | *
82 | * @param {Replacement} repl - The associated Replacement.
83 | * @param {CorrsGraphic} parent - The collection that owns this graphic.
84 | *
85 | */
86 | constructor(repl, parent)
87 | {
88 | this.replacement = repl;
89 | this.parent = parent;
90 | this.drawParams = {};
91 | }
92 |
93 |
94 | /**
95 | * Draws the Replacement line.
96 | *
97 | */
98 | redraw(ctx)
99 | {
100 | // Update our drawing parameters if necessary
101 | if ( Namespace.UiUtils.NeedToRescale(this.drawParams, ctx) ) {
102 | if (!this._updateDrawParams(ctx)) { return; }
103 | }
104 |
105 | const dp = this.drawParams;
106 | ctx.strokeStyle = this.parent.wkspUi.replColor;
107 | ctx.lineWidth = 1;
108 | ctx.setLineDash([]);
109 |
110 | ctx.beginPath();
111 | ctx.ellipse(dp.cx, dp.cy, dp.radX, dp.radY, dp.rotAngle, dp.startAngle, dp.endAngle);
112 | ctx.stroke();
113 | }
114 |
115 |
116 | /**
117 | * Recalculates drawing parameters.
118 | * @private
119 | */
120 | _updateDrawParams(ctx)
121 | {
122 | const [w, h] = [ctx.canvas.width, ctx.canvas.height];
123 | if ((w < 1) || (h < 1)) { return false; }
124 |
125 | const dp = this.drawParams;
126 | dp.canvasWidth = w; dp.canvasHeight = h;
127 |
128 | const wkspUi = this.parent.wkspUi;
129 | const objA = this.replacement.objFromInitial;
130 | const objB = this.replacement.objFromModified;
131 | const initialGraphic = wkspUi.initialStringGraphic.getChildGraphic(objA);
132 | const modifiedGraphic = wkspUi.modifiedStringGraphic.getChildGraphic(objB);
133 | if (!initialGraphic || !modifiedGraphic) {
134 | dp.canvasWidth = dp.canvasHeight = 0;
135 | return false;
136 | }
137 |
138 | const pti = initialGraphic.drawParams.attachPoints['repl'];
139 | const ptm = modifiedGraphic.drawParams.attachPoints['repl'];
140 | dp.cx = (pti.x + ptm.x) / 2;
141 | dp.cy = Math.min(pti.y, ptm.y);
142 | dp.radX = Math.abs(ptm.x - pti.x) / 2;
143 | dp.radY = h/10;
144 | dp.rotAngle = 0;
145 | dp.startAngle = Math.PI;
146 | dp.endAngle = 2 * Math.PI;
147 |
148 | return true;
149 | }
150 |
151 | };
152 |
153 |
154 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/LettersGraphic.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for drawing the letters of a given string.
9 | *
10 | */
11 | Namespace.LettersGraphic = class
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {StringGraphic} stringGraphic - The parent graphic.
17 | *
18 | */
19 | constructor(stringGraphic)
20 | {
21 | this.stringGraphic = stringGraphic;
22 | this.wkspUi = stringGraphic.wkspUi;
23 |
24 | // Create my letter graphics
25 | this.letterGraphics = [];
26 |
27 | if (stringGraphic.quadrant != 2) {
28 | const wstring = this.stringGraphic.wstring;
29 | for (let i=0; i lg.redraw(ctx));
52 | }
53 | };
54 |
55 |
56 | /**
57 | * @classdesc
58 | * This class draws a single letter from a WorkspaceString.
59 | *
60 | */
61 | Namespace.LetterGraphic = class
62 | {
63 | /**
64 | * @constructor
65 | *
66 | * @param {Letter} letter - The wrapped letter.
67 | * @param {Number} index - The letter's index in its string.
68 | * @param {StringGraphic} parent - The StringGraphic that owns this object.
69 | */
70 | constructor(letter, index, parent)
71 | {
72 | if (letter instanceof Namespace.Letter) {
73 | this.letter = letter;
74 | this.char = letter.char;
75 | } else {
76 | this.letter = null;
77 | this.char = letter;
78 | }
79 |
80 | this.index = index;
81 | this.parent = parent;
82 | this.stringGraphic = parent.stringGraphic;
83 | this.wkspUi = parent.wkspUi;
84 | this.drawParams = {};
85 | }
86 |
87 |
88 | redraw(ctx)
89 | {
90 | // Update our drawing parameters if necessary
91 | if (Namespace.UiUtils.NeedToRescale(this.drawParams, ctx)) {
92 | this._updateDrawParams(ctx);
93 | }
94 |
95 | const wkspUi = this.wkspUi;
96 | const dp = this.drawParams;
97 | ctx.font = dp.font;
98 | ctx.textAlign = 'left';
99 | ctx.fillStyle = (this.stringGraphic == wkspUi.answerStringGraphic) ? wkspUi.answerLetterColor : wkspUi.letterColor;
100 | ctx.fillText(this.char, dp.x, dp.y);
101 | }
102 |
103 |
104 | /**
105 | * Recalculates the drawing parameters for this object.
106 | * @private
107 | *
108 | */
109 | _updateDrawParams(ctx)
110 | {
111 | const [w, h] = [ctx.canvas.width, ctx.canvas.height];
112 | if ((w < 1) || (h < 1)) { return false; }
113 |
114 | const dp = this.drawParams;
115 | dp.canvasWidth = w; dp.canvasHeight = h;
116 |
117 | // Set the font parameters
118 | const stringDp = this.parent.stringGraphic.drawParams;
119 | dp.fontSize = stringDp.fontSize;
120 | dp.font = stringDp.font;
121 | ctx.font = dp.font; // Must set the font before measuring text
122 |
123 | // Calculate the letter's bounding box
124 | const charMetrics = ctx.measureText(this.char);
125 | dp.charWidth = charMetrics.actualBoundingBoxLeft + charMetrics.actualBoundingBoxRight;
126 | const charHeight = charMetrics.fontBoundingBoxAscent;
127 |
128 | dp.x = (this.index === 0) ? stringDp.stringStartX :
129 | this.parent.letterGraphics[this.index-1].drawParams.bbox.r + stringDp.charSpacing;
130 | dp.y = stringDp.baselineY;
131 |
132 | dp.bbox = {
133 | l: dp.x - 0.5*charMetrics.actualBoundingBoxDescent,
134 | r: dp.x - 0.5*charMetrics.actualBoundingBoxDescent + dp.charWidth,
135 | t: dp.y - charMetrics.actualBoundingBoxAscent,
136 | b: dp.y + charMetrics.actualBoundingBoxDescent
137 | };
138 |
139 | dp.attachPoints = {
140 | correspTop: {x: (dp.bbox.l + dp.bbox.r)/2 , y: dp.bbox.t-charHeight/4},
141 | correspBtm: {x: (dp.bbox.l + dp.bbox.r)/2 , y: dp.bbox.b+charHeight/4},
142 | correspRight: {x: dp.bbox.r+charHeight/4, y: (dp.bbox.t + dp.bbox.b)/2},
143 | repl: {x: (dp.bbox.l + dp.bbox.r)/2 , y: stringDp.top - charHeight/4},
144 | bondR1: {x: dp.bbox.r + charHeight/12, y: stringDp.top + charHeight/3},
145 | bondL1: {x: dp.bbox.l , y: stringDp.top + charHeight/3}
146 | };
147 | }
148 |
149 | };
150 |
151 |
152 |
153 |
154 |
155 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/CodeletFactory.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This class provides a factory method for creating codelets.
10 | *
11 | */
12 | Namespace.Codelets.CodeletFactory = class
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {Copycat} ctx - The Copycat instance.
18 | */
19 | constructor(ctx)
20 | {
21 | this.ctx = ctx;
22 | }
23 |
24 |
25 | /**
26 | * Creates a codelet given its name and constructor arguments.
27 | *
28 | * @param {string} name - The name of the codelet.
29 | * @param {Number} urgency - The urgency of the codelet.
30 | * @param {Array} codeletArgs - Arguments to pass to the codelet.
31 | * @param {Number} birthdate - The birthdate of the codelet.
32 | *
33 | */
34 | create(name, urgency, codeletArgs, birthdate)
35 | {
36 | if (!birthdate && birthdate !== 0) {
37 | birthdate = this.ctx.coderack.numCodeletsRun;
38 | }
39 |
40 | const NS = Namespace.Codelets;
41 | const ctorArgs = [this.ctx, urgency, codeletArgs, birthdate];
42 |
43 | let result = null;
44 | switch (name)
45 | {
46 | case 'bond-builder':
47 | result = new NS.BondBuilder(...ctorArgs);
48 | break;
49 |
50 | case 'bond-strength-tester':
51 | result = new NS.BondStrengthTester(...ctorArgs);
52 | break;
53 |
54 | case 'bottom-up-bond-scout':
55 | result = new NS.BottomUpBondScout(...ctorArgs);
56 | break;
57 |
58 | case 'bottom-up-correspondence-scout':
59 | result = new NS.BottomUpCorrespondenceScout(...ctorArgs);
60 | break;
61 |
62 | case 'bottom-up-description-scout':
63 | result = new NS.BottomUpDescriptionScout(...ctorArgs);
64 | break;
65 |
66 | case 'breaker':
67 | result = new NS.Breaker(...ctorArgs);
68 | break;
69 |
70 | case 'correspondence-builder':
71 | result = new NS.CorrespondenceBuilder(...ctorArgs);
72 | break;
73 |
74 | case 'correspondence-strength-tester':
75 | result = new NS.CorrespondenceStrengthTester(...ctorArgs);
76 | break;
77 |
78 | case 'description-builder':
79 | result = new NS.DescriptionBuilder(...ctorArgs);
80 | break;
81 |
82 | case 'description-strength-tester':
83 | result = new NS.DescriptionStrengthTester(...ctorArgs);
84 | break;
85 |
86 | case 'group-builder':
87 | result = new NS.GroupBuilder(...ctorArgs);
88 | break;
89 |
90 | case 'group-strength-tester':
91 | result = new NS.GroupStrengthTester(...ctorArgs);
92 | break;
93 |
94 | case 'important-object-correspondence-scout':
95 | result = new NS.ImportantObjectCorrespondenceScout(...ctorArgs);
96 | break;
97 |
98 | case 'replacement-finder':
99 | result = new NS.ReplacementFinder(...ctorArgs);
100 | break;
101 |
102 | case 'rule-builder':
103 | result = new NS.RuleBuilder(...ctorArgs);
104 | break;
105 |
106 | case 'rule-scout':
107 | result = new NS.RuleScout(...ctorArgs);
108 | break;
109 |
110 | case 'rule-strength-tester':
111 | result = new NS.RuleStrengthTester(...ctorArgs);
112 | break;
113 |
114 | case 'rule-translator':
115 | result = new NS.RuleTranslator(...ctorArgs);
116 | break;
117 |
118 | case 'top-down-bond-scout--category':
119 | result = new NS.TopDownBondScout_Category(...ctorArgs);
120 | break;
121 |
122 | case 'top-down-bond-scout--direction':
123 | result = new NS.TopDownBondScout_Direction(...ctorArgs);
124 | break;
125 |
126 | case 'top-down-description-scout':
127 | result = new NS.TopDownDescriptionScout(...ctorArgs);
128 | break;
129 |
130 | case 'top-down-group-scout--category':
131 | result = new NS.TopDownGroupScout_Category(...ctorArgs);
132 | break;
133 |
134 | case 'top-down-group-scout--direction':
135 | result = new NS.TopDownGroupScout_Direction(...ctorArgs);
136 | break;
137 |
138 | case 'group-scout--whole-string':
139 | result = new NS.GroupScout_WholeString(...ctorArgs);
140 | break;
141 |
142 | default:
143 | throw new Error('Unknown codelet name: ' + name);
144 | }
145 | return result;
146 | }
147 | };
148 |
149 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/BondBuilder.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This codelet tries to build a proposed bond, fighting with any
10 | * incompatible structures if necessary.
11 | *
12 | */
13 | Namespace.Codelets.BondBuilder = class extends Namespace.Codelets.CodeletBase
14 | {
15 | /**
16 | * @constructor
17 | *
18 | * @param {Copycat} ctx - The Copycat instance.
19 | * @param {Number} urgency - The urgency of the codelet.
20 | * @param {Array} args - Arguments to pass to the codelet.
21 | * @param {Number} birthdate - The birthdate of the codelet.
22 | */
23 | constructor(ctx, urgency, args, birthdate)
24 | {
25 | super('bond-builder', ctx, urgency, birthdate);
26 | this.bond = args[0];
27 | }
28 |
29 |
30 | /**
31 | * Runs the codelet.
32 | */
33 | run()
34 | {
35 | const ctx = this.ctx;
36 | const bond = this.bond;
37 | bond.updateStrength();
38 |
39 | // Fizzle if the bond's objects are gone
40 | const wobjs = ctx.workspace.objects;
41 | if (!wobjs.includes(bond.source) || !wobjs.includes(bond.destination)) {
42 | return;
43 | }
44 |
45 | // Fizzle if the bond already exists
46 | if (bond.string.bonds.some(sbond => this._sameNeighbors(bond, sbond) && this._sameCategories(bond, sbond))) {
47 | bond.category.activation = 100;
48 | if (bond.directionCategory) { bond.directionCategory.activation = 100; }
49 | return;
50 | }
51 |
52 | // Provide UI feedback
53 | if (ctx.ui && !ctx.batchMode) {
54 | ctx.ui.workspaceUi.getStringGraphic(bond.string).bondsGraphic.flashProposed(bond);
55 | }
56 |
57 | // Fight it out with any incompatible bonds
58 | const Utils = Namespace.Codelets.CodeletUtils;
59 | const incompatibleBonds = bond.string.bonds.filter(b => this._sameNeighbors(bond,b));
60 | if (!Utils.fightItOut(bond, incompatibleBonds, 1.0, 1.0)) {
61 | return;
62 | }
63 |
64 | // Fight it out with any incompatible groups
65 | const incompatibleGroups = bond.source.getCommonGroups(bond.destination);
66 | if (!Utils.fightItOut(bond, incompatibleGroups, 1.0, 1.0)) {
67 | return;
68 | }
69 |
70 | // Fight it out with any incompatible correspondences
71 | const incompatibleCorrespondences = [];
72 | if (bond.leftObject.leftmost || bond.rightObject.rightmost) {
73 | if (bond.directionCategory) {
74 | const incompatibleCorrespondences = this._getIncompatibleCorrespondences(bond);
75 | if (incompatibleCorrespondences.length > 0) {
76 | if (!Utils.fightItOut(bond, incompatibleCorrespondences, 2.0, 3.0)) {
77 | return;
78 | }
79 | }
80 | }
81 | }
82 |
83 | // We won! Destroy the incompatibles and build our bond.
84 | incompatibleBonds.forEach(x => x.break());
85 | incompatibleGroups.forEach(x => x.break());
86 | incompatibleCorrespondences.forEach(x => x.break());
87 |
88 | bond.build();
89 | }
90 |
91 |
92 | /**
93 | * Checks whether two bonds have the same neighbors.
94 | * @private
95 | *
96 | */
97 | _sameNeighbors(bond1, bond2)
98 | {
99 | return (bond1.leftObject == bond2.leftObject) && (bond1.rightObject == bond2.rightObject);
100 | }
101 |
102 |
103 | /**
104 | * Checks whether two bonds have the same categories.
105 | * @private
106 | *
107 | */
108 | _sameCategories(bond1, bond2)
109 | {
110 | return (bond1.category == bond2.category) && (bond1.directionCategory == bond2.directionCategory);
111 | }
112 |
113 |
114 | /**
115 | * Returns a list of correspondences that are incompatible with a given bond.
116 | * @private
117 | */
118 | _getIncompatibleCorrespondences(bond)
119 | {
120 | const incompatibles = [];
121 | if (bond.leftObject.leftmost && bond.leftObject.correspondence)
122 | {
123 | const obj = (bond.string == bond.wksp.initialWString) ?
124 | bond.leftObject.correspondence.objFromTarget : bond.leftObject.correspondence.objFromInitial;
125 |
126 | if (obj.leftmost && obj.rightBond && obj.rightBond.directionCategory) {
127 | if (obj.rightBond.directionCategory != bond.directionCategory) {
128 | incompatibles.push(bond.leftObject.correspondence);
129 | }
130 | }
131 | }
132 | if (bond.rightObject.rightmost && bond.rightObject.correspondence)
133 | {
134 | const obj = (bond.string == bond.wksp.initialWString) ?
135 | bond.rightObject.correspondence.objFromTarget : bond.rightObject.correspondence.objFromInitial;
136 |
137 | if (obj.rightmost && obj.leftBond && obj.leftBond.directionCategory) {
138 | if (obj.leftBond.directionCategory != bond.directionCategory) {
139 | incompatibles.push(bond.rightObject.correspondence);
140 | }
141 | }
142 | }
143 | return incompatibles;
144 | }
145 |
146 |
147 | };
148 |
149 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Workspace.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * The Workspace is where Copycat builds its perceptual structures.
9 | *
10 | */
11 | Namespace.Workspace = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {Copycat} ctx - The Copycat instance.
17 | * @param {String} [initialString] - The 'A' in A:B -> C:D
18 | * @param {String} [modifiedString] - The 'B' in A:B -> C:D
19 | * @param {String} [targetString] - The 'C' in A:B -> C:D
20 | */
21 | constructor(ctx, initialString='abc', modifiedString='abd', targetString='pqr')
22 | {
23 | this.ctx = ctx;
24 | this.objects = [];
25 | this.structures = [];
26 | this.changedObject = null;
27 | this.rule = null;
28 | this.intraStringUnhappiness = 0;
29 | this.interStringUnhappiness = 0;
30 |
31 | this.initialWString = new Namespace.WorkspaceString(this, initialString?.toLowerCase() || '');
32 | this.modifiedWString = new Namespace.WorkspaceString(this, modifiedString?.toLowerCase() || '');
33 | this.targetWString = new Namespace.WorkspaceString(this, targetString?.toLowerCase() || '');
34 | this.finalAnswer = null; // A javascript string
35 | }
36 |
37 |
38 | /**
39 | * Returns a string describing the object.
40 | */
41 | synopsis()
42 | {
43 | return '';
45 | }
46 |
47 |
48 | /**
49 | * Resets the workspace to its initial state, optionally modifying the
50 | * input strings.
51 | *
52 | * @param {String} [initialString] - The 'A' in A:B -> C:D
53 | * @param {String} [modifiedString] - The 'B' in A:B -> C:D
54 | * @param {String} [targetString] - The 'C' in A:B -> C:D
55 | *
56 | */
57 | reset(initialString=null, modifiedString=null, targetString=null)
58 | {
59 | // Clear the workspace
60 | this.finalAnswer = null;
61 | this.changedObject = null;
62 | this.objects = [];
63 | this.structures = [];
64 | this.rule = null;
65 | this.intraStringUnhappiness = 0;
66 | this.interStringUnhappiness = 0;
67 |
68 | // Create or reset the WorkspaceStrings
69 | this.initialWString = new Namespace.WorkspaceString(this, initialString?.toLowerCase() || this.initialWString.jstring);
70 | this.modifiedWString = new Namespace.WorkspaceString(this, modifiedString?.toLowerCase() || this.modifiedWString.jstring);
71 | this.targetWString = new Namespace.WorkspaceString(this, targetString?.toLowerCase() || this.targetWString.jstring);
72 | }
73 |
74 |
75 | /**
76 | * Updates the structure strengths, and the happiness, importance,
77 | * and salience of all objects and strings in the workspace.
78 | *
79 | */
80 | updateEverything()
81 | {
82 | // Update structures
83 | for (let structure of this.structures) {
84 | structure.updateStrength();
85 | }
86 |
87 | // Update objects
88 | for (let obj of this.objects) {
89 | obj.updateValues();
90 | }
91 |
92 | // Update strings
93 | this.initialWString.updateRelativeImportances();
94 | this.targetWString.updateRelativeImportances();
95 | this.initialWString.updateIntraStringUnhappiness();
96 | this.targetWString.updateIntraStringUnhappiness();
97 | }
98 |
99 |
100 | /**
101 | * Updates the string unhappiness values and then uses them to
102 | * calculate the current workspace temperature.
103 | *
104 | */
105 | calcTemperature()
106 | {
107 | // First, update my happiness values
108 | this.intraStringUnhappiness =
109 | Math.min(100, 0.5 * this.objects.map(o => o.relativeImportance * o.intraStringUnhappiness).reduce((a,b) => a+b, 0));
110 |
111 | this.interStringUnhappiness =
112 | Math.min(100, 0.5 * this.objects.map(o => o.relativeImportance * o.interStringUnhappiness).reduce((a,b) => a+b, 0));
113 |
114 | const totalUnhappiness =
115 | Math.min(100, 0.5 * this.objects.map(o => o.relativeImportance * o.totalUnhappiness).reduce((a,b) => a+b, 0));
116 |
117 | // Now, calculate the temperature
118 | let ruleWeakness = 100;
119 | if (this.rule) {
120 | this.rule.updateStrength();
121 | ruleWeakness = 100 - this.rule.totalStrength;
122 | }
123 | const temperature = 0.8*totalUnhappiness + 0.2*ruleWeakness;
124 | return temperature;
125 | }
126 |
127 |
128 | /**
129 | * Gets all the concept mappings in the workspace that permit slippage.
130 | *
131 | */
132 | getSlippableMappings()
133 | {
134 | const result = [];
135 | if (this.changedObject && this.changedObject.correspondence) {
136 | result.push(...this.changedObject.correspondence.conceptMappings);
137 | }
138 |
139 | const corresps = this.initialWString.objects.filter(o => o.correspondence).map(o => o.correspondence);
140 | corresps.forEach(
141 | corresp => result.push(...corresp.getSlippableMappings().filter(m => !m.isNearlyContainedIn(result)))
142 | );
143 |
144 | return result;
145 | }
146 |
147 | };
148 |
149 |
150 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/GroupBuilder.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * This codelet tries to build a proposed Group, fighting against
9 | * competitors if necessary.
10 | *
11 | */
12 | Namespace.Codelets.GroupBuilder = class extends Namespace.Codelets.CodeletBase
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {Copycat} ctx - The Copycat instance.
18 | * @param {Number} urgency - The urgency of the codelet.
19 | * @param {Array} args - Arguments to pass to the codelet.
20 | * @param {Number} birthdate - The birthdate of the codelet.
21 | */
22 | constructor(ctx, urgency, args, birthdate)
23 | {
24 | super('group-builder', ctx, urgency, birthdate);
25 | this.group = args[0];
26 | }
27 |
28 |
29 | /**
30 | * Runs the codelet.
31 | */
32 | run()
33 | {
34 | const ctx = this.ctx;
35 | const wksp = ctx.workspace;
36 | const sn = ctx.slipnet;
37 | const CodeletUtils = Namespace.Codelets.CodeletUtils;
38 | const group = this.group;
39 |
40 | // If an equivalent group already exists, just activate it.
41 | const equivalent = group.string.getEquivalentGroup(group);
42 | if (equivalent) {
43 | group.descriptions.forEach(d => d.descriptor.activation = 100);
44 | equivalent.addDescriptions(group.descriptions);
45 | return;
46 | }
47 |
48 | // Check to see if all objects are still there
49 | if (group.objectList.some(o => !wksp.objects.includes(o))) {
50 | return;
51 | }
52 |
53 | // Provide UI feedback
54 | if (ctx.ui && !ctx.batchMode) {
55 | ctx.ui.workspaceUi.getStringGraphic(group.string).groupsGraphic.flashProposed(group);
56 | }
57 |
58 | // Check to see if bonds have the same direction
59 | const incompatibleBonds = [];
60 | if (group.objectList.length > 1) {
61 | let previous = group.objectList[0];
62 | for (let obj of group.objectList.slice(1)) {
63 | const leftBond = obj.leftBond;
64 | if (leftBond) {
65 | if (leftBond.leftObject == previous) {
66 | continue;
67 | }
68 | if (leftBond.directionCategory == group.directionCategory) {
69 | continue;
70 | }
71 | incompatibleBonds.push(leftBond);
72 | }
73 | previous = obj;
74 | }
75 |
76 | const n = group.objectList.length;
77 | let next = group.objectList[n-1];
78 | for (let i=n-2; i>=0; i--) { // Don't use reverse(), as it changes the array
79 | const obj = group.objectList[i];
80 | const rightBond = obj.rightBond;
81 | if (rightBond) {
82 | if (rightBond.rightObject == next) {
83 | continue;
84 | }
85 | if (rightBond.directionCategory == group.directionCategory) {
86 | continue;
87 | }
88 | incompatibleBonds.push(rightBond);
89 | }
90 | next = obj;
91 | }
92 | }
93 |
94 | // If incompatible bonds exist then fight
95 | group.updateStrength();
96 | if (!CodeletUtils.fightItOut(group, incompatibleBonds, 1.0, 1.0)) {
97 | return;
98 | }
99 |
100 | // If incompatible groups exist then fight
101 | const incompatibleGroups = this._getIncompatibleGroups(group);
102 | if (!CodeletUtils.fightItOut(group, incompatibleGroups, 1.0, 1.0)) {
103 | return;
104 | }
105 |
106 | // Break incompatible bonds
107 | incompatibleBonds.forEach(b => b.break());
108 |
109 | // Create new bonds
110 | let source, dest;
111 | group.bondList = [];
112 | for (let i=1; i g.break());
125 | group.build();
126 | group.descriptions.forEach(d => d.descriptor.activation = 100);
127 | }
128 |
129 |
130 | /**
131 | * Gets all groups that have an object in common with
132 | * the given one.
133 | * @private
134 | */
135 | _getIncompatibleGroups(group)
136 | {
137 | const result = [];
138 | for (let obj of group.objectList) {
139 | while (obj.group) {
140 | if (obj.group != group) {
141 | result.push(obj.group);
142 | obj = obj.group;
143 | }
144 | }
145 | }
146 | return result;
147 | }
148 |
149 |
150 | };
151 |
152 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/TopDownGroupScout_Category.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 | /**
7 | * @classdesc
8 | * This codelet seeks potential Groups in the initial and target strings.
9 | *
10 | */
11 | Namespace.Codelets.TopDownGroupScout_Category = class extends Namespace.Codelets.CodeletBase
12 | {
13 | /**
14 | * @constructor
15 | *
16 | * @param {Copycat} ctx - The Copycat instance.
17 | * @param {Number} urgency - The urgency of the codelet.
18 | * @param {Array} args - Arguments to pass to the codelet.
19 | * @param {Number} birthdate - The birthdate of the codelet.
20 | */
21 | constructor(ctx, urgency, args, birthdate)
22 | {
23 | super('top-down-group-scout--category', ctx, urgency, birthdate);
24 | this.groupCategory = args[0];
25 | }
26 |
27 |
28 | /**
29 | * Runs the codelet.
30 | */
31 | run()
32 | {
33 | const ctx = this.ctx;
34 | const sn = ctx.slipnet;
35 | const CodeletUtils = Namespace.Codelets.CodeletUtils;
36 |
37 | const cat = this.groupCategory.getRelatedNode(sn.bondCategory);
38 | if (!cat) { return; }
39 |
40 | let source = CodeletUtils.getScoutSource(ctx, 'bondCategory', cat);
41 | if (!source || source.spansString()) { return; }
42 |
43 | let direction = source.leftmost ? sn.right : source.rightmost ? sn.left :
44 | ctx.randGen.weightedChoice([sn.left, sn.right], [sn.left.activation, sn.right.activation]);
45 |
46 | let firstBond = (direction == sn.left) ? source.leftBond : source.rightBond;
47 |
48 | if (!firstBond || (firstBond.category != cat)) {
49 | // Check other side of object
50 | firstBond = (direction == sn.right) ? source.leftBond : source.rightBond;
51 | if (!firstBond || (firstBond.category != cat)) {
52 | if ((cat == sn.sameness) && (source instanceof Namespace.Letter)) {
53 | if (ctx.randGen.coinFlip(this._singleLetterGroupProbability(source))) {
54 | // Propose a single-letter group
55 | ctx.coderack.proposeGroup([source], [], sn.samenessGroup, null, sn.letterCategory);
56 | return;
57 | }
58 | }
59 | }
60 | return;
61 | }
62 |
63 | direction = firstBond.directionCategory;
64 | let search = true;
65 | let bondFacet = null;
66 | // Find leftmost object in group with these bonds
67 | while (search) {
68 | search = false;
69 | if (!source.leftBond) {
70 | continue;
71 | }
72 | if (source.leftBond.category != cat) {
73 | continue;
74 | }
75 | if (source.leftBond.directionCategory != direction) {
76 | if (source.leftBond.directionCategory) {
77 | continue;
78 | }
79 | }
80 | if (!bondFacet || (bondFacet == source.leftBond.facet)) {
81 | bondFacet = source.leftBond.facet;
82 | direction = source.leftBond.directionCategory;
83 | source = source.leftBond.leftObject;
84 | search = true;
85 | }
86 | }
87 |
88 | // Find rightmost object in group with these bonds
89 | search = true;
90 | let destination = source;
91 | while (search) {
92 | search = false;
93 | if (!destination.rightBond) {
94 | continue;
95 | }
96 | if (destination.rightBond.category != cat) {
97 | continue;
98 | }
99 | if (destination.rightBond.directionCategory != direction) {
100 | if (destination.rightBond.directionCategory) {
101 | continue;
102 | }
103 | }
104 | if (!bondFacet || (bondFacet == destination.rightBond.facet)) {
105 | bondFacet = destination.rightBond.facet;
106 | direction = source.rightBond.directionCategory;
107 | destination = destination.rightBond.rightObject;
108 | search = true;
109 | }
110 | }
111 | if (destination == source) { return; }
112 |
113 | const objects = [source];
114 | const bonds = [];
115 | while (source != destination) {
116 | bonds.push(source.rightBond);
117 | objects.push(source.rightBond.rightObject);
118 | source = source.rightBond.rightObject;
119 | }
120 | ctx.coderack.proposeGroup(objects, bonds, this.groupCategory, direction, bondFacet);
121 | }
122 |
123 |
124 |
125 | /**
126 | * Calculates the probability of a single letter group.
127 | * @private
128 | */
129 | _singleLetterGroupProbability(letter)
130 | {
131 | const sn = this.ctx.slipnet;
132 | const group = new Namespace.Group(letter.string, sn.samenessGroup, null, sn.letterCategory, [letter], []);
133 |
134 | const numSupporters = group._numberOfLocalSupportingGroups();
135 | if (numSupporters === 0) {
136 | return 0.0;
137 | }
138 |
139 | const exp = (numSupporters == 1) ? 4.0 : (numSupporters == 2) ? 2.0 : 1.0;
140 | const support = group._localSupport() / 100;
141 | const activation = sn.length.activation / 100;
142 | const supportedActivation = Math.pow((support * activation), exp);
143 | return this.ctx.temperature.getAdjustedProb(supportedActivation);
144 | }
145 | };
146 |
147 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/StringGraphic.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class is responsible for drawing the workspace strings
9 | * along with their bonds, groups, and descriptions.
10 | *
11 | */
12 | Namespace.StringGraphic = class
13 | {
14 | /**
15 | * @constructor
16 | *
17 | * @param {WorkspaceUi} workspaceUi - The parent Ui.
18 | * @param {Number} quadrant - The string's quadrant.
19 | *
20 | */
21 | constructor(workspaceUi, quadrant)
22 | {
23 | this.wkspUi = workspaceUi;
24 | this.quadrant = quadrant;
25 | this.wstring = null;
26 | this.jstring = "";
27 |
28 | this.lettersGraphic = null;
29 | this.descriptionsGraphic = null;
30 | this.bondsGraphic = null;
31 | this.groupsGraphic = null;
32 |
33 | this.drawParams = {};
34 |
35 | this._updateStringFromWorkspace();
36 | this._createChildGraphics();
37 | }
38 |
39 |
40 | /**
41 | * Creates the child graphics objects.
42 | * @private
43 | *
44 | */
45 | _createChildGraphics()
46 | {
47 | this.lettersGraphic = new Namespace.LettersGraphic(this);
48 |
49 | if (this.quadrant != 2) {
50 | this.descriptionsGraphic = new Namespace.DescriptionsGraphic(this);
51 | this.bondsGraphic = new Namespace.BondsGraphic(this);
52 | this.groupsGraphic = new Namespace.GroupsGraphic(this);
53 | }
54 | }
55 |
56 |
57 | /**
58 | * Gets the LetterGraphic or GroupGraphic associated with
59 | * a given Letter or Group.
60 | *
61 | * @param {Letter|Group} wrappedObject - The search key.
62 | */
63 | getChildGraphic(wrappedObject)
64 | {
65 | if (wrappedObject instanceof Namespace.Letter) {
66 | return this.lettersGraphic.letterGraphics.find( lg => lg.letter == wrappedObject );
67 | }
68 | else if (wrappedObject instanceof Namespace.Group) {
69 | return this.groupsGraphic.groupGraphics.find( g => g.group == wrappedObject );
70 | }
71 | return null;
72 | }
73 |
74 |
75 | /**
76 | * Redraws everything
77 | *
78 | */
79 | redraw(ctx)
80 | {
81 | // Check whether our wrapped string has changed
82 | let stringChanged = false;
83 | if ( this._updateStringFromWorkspace() ) {
84 | this._createChildGraphics();
85 | stringChanged = true;
86 | }
87 |
88 | // Update our drawing parameters if necessary
89 | const UiUtils = Namespace.UiUtils;
90 | if (stringChanged || UiUtils.NeedToRescale(this.drawParams, ctx)) {
91 | this._updateDrawParams(ctx);
92 | }
93 |
94 | // Draw our child graphics
95 | // (Note that the drawing logic assumes the drawing order shown here.)
96 | [this.lettersGraphic, this.descriptionsGraphic, this.groupsGraphic, this.bondsGraphic].forEach( g => {
97 | if (g) { g.redraw(ctx); }
98 | });
99 | }
100 |
101 |
102 | /**
103 | * Check whether the wrapped string has changed.
104 | * @private
105 | *
106 | */
107 | _updateStringFromWorkspace()
108 | {
109 | const wksp = this.wkspUi.workspace;
110 | const q = this.quadrant;
111 |
112 | let changed = false;
113 | if (q == 2) {
114 | const jstring = wksp.finalAnswer || '?';
115 | if (this.jstring != jstring) {
116 | this.jstring = jstring;
117 | changed = true;
118 | }
119 | }
120 | else {
121 | const wstring = (q == 0) ? wksp.initialWString :
122 | (q == 1) ? wksp.modifiedWString : wksp.targetWString;
123 | if (this.wstring != wstring) {
124 | this.wstring = wstring;
125 | this.jstring = wstring ? wstring.jstring : '';
126 | changed = true;
127 | }
128 | }
129 | return changed;
130 | }
131 |
132 |
133 | /**
134 | * Recalculates the drawing parameters.
135 | * @private
136 | *
137 | */
138 | _updateDrawParams(ctx)
139 | {
140 | const [w, h] = [ctx.canvas.width, ctx.canvas.height];
141 | if ((w < 1) || (h < 1)) { return false; }
142 |
143 | const dp = this.drawParams;
144 | dp.canvasWidth = w; dp.canvasHeight = h;
145 |
146 |
147 | const wksp = this.wkspUi.workspace;
148 | const inputStrings = [wksp.initialWString, wksp.modifiedWString,
149 | wksp.targetWString];
150 | const maxChars = Math.max(...inputStrings.map(s => s.jstring.length));
151 | dp.fontSize = Math.round(1.1*Math.min(h/18, w/(2.5*maxChars)));
152 | dp.font = 'italic bold ' + dp.fontSize.toString() + 'px serif';
153 | ctx.font = dp.font; // Must set the font before measuring text
154 |
155 | dp.baselineY = (this.quadrant < 2) ? h/3 : 2*h/3;
156 |
157 | dp.emWidth = ctx.measureText('m').width;
158 |
159 | dp.stringCenterX = (this.quadrant == 0 || this.quadrant == 3) ? w/4 - w/80 : 3*w/4 + w/80;
160 |
161 | const charMetrics = this.jstring.split('').map( c => ctx.measureText(c) );
162 | const sumOfCharWidths = charMetrics.reduce( (a,b) => a + b.actualBoundingBoxLeft + b.actualBoundingBoxRight, 0 );
163 |
164 | const nChars = this.jstring.length;
165 | dp.charSpacing = Math.min(5*sumOfCharWidths/nChars, (0.40*w - sumOfCharWidths)/(nChars-1));
166 |
167 | dp.stringWidth = sumOfCharWidths + dp.charSpacing*(nChars-1);
168 | dp.stringStartX = dp.stringCenterX - dp.stringWidth/2;
169 |
170 | dp.maxCharAscent = Math.max(...(charMetrics.map(m => m.actualBoundingBoxAscent)));
171 | dp.maxCharDescent = Math.max(...(charMetrics.map(m => m.actualBoundingBoxDescent)));
172 | dp.top = dp.baselineY - dp.maxCharAscent;
173 | }
174 | };
175 |
176 |
177 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/UiUtils.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class provides several utility functions needed by the UI classes.
9 | *
10 | */
11 | Namespace.UiUtils = class {
12 | constructor() { }
13 | };
14 |
15 |
16 | /**
17 | * Creates an html element and appends it to the parent.
18 | *
19 | */
20 | Namespace.UiUtils.CreateElement = function(type, id, parent, styles, props)
21 | {
22 | const elem = document.createElement(type);
23 | elem.id = id;
24 | if (parent) { parent.append(elem); }
25 |
26 | if (styles) {
27 | for (let styName in styles) {
28 | if (Object.prototype.hasOwnProperty.call(styles, styName)) {
29 | let val = styles[styName];
30 | if (typeof val === 'number') { val = val.toString() + 'px'; }
31 | elem.style[styName] = val;
32 | }
33 | }
34 | }
35 | if (props) {
36 | for (let propName in props) {
37 | if (Object.prototype.hasOwnProperty.call(props, propName)) {
38 | elem[propName] = props[propName];
39 | }
40 | }
41 | }
42 |
43 | // Set some default styles
44 | if (!styles || !Object.prototype.hasOwnProperty.call(styles, 'position')) { elem.style.position = 'absolute'; }
45 | if (!styles || !Object.prototype.hasOwnProperty.call(styles, 'margin')) { elem.style.margin = '0px'; }
46 | if (!styles || !Object.prototype.hasOwnProperty.call(styles, 'padding')) { elem.style.padding = '0px'; }
47 |
48 | return elem;
49 | };
50 |
51 |
52 | /**
53 | * Applies styling to an html element
54 | * @static
55 | *
56 | */
57 | Namespace.UiUtils.StyleElement = function(elem, styles)
58 | {
59 | for (let propName in styles) {
60 | if (Object.prototype.hasOwnProperty.call(styles, propName)) {
61 | let val = styles[propName];
62 | if (typeof val === 'number') { val = val.toString() + 'px'; }
63 | elem.style[propName] = val;
64 | }
65 | }
66 |
67 | // Set some default styles
68 | if (!Object.prototype.hasOwnProperty.call(styles, 'position')) { elem.style.position = 'absolute'; }
69 | if (!Object.prototype.hasOwnProperty.call(styles, 'margin')) { elem.style.margin = '0px'; }
70 | if (!Object.prototype.hasOwnProperty.call(styles, 'padding')) { elem.style.padding = '0px'; }
71 |
72 | return elem;
73 | };
74 |
75 |
76 | /**
77 | * Tries to determine whether we're on a touch device.
78 | * @static
79 | *
80 | */
81 | Namespace.UiUtils.isTouchDevice = function()
82 | {
83 | // This test is fallible, but it seems to be the best we can do.
84 | return ( ('ontouchstart' in document.documentElement) || window.navigator.msMaxTouchPoints );
85 | };
86 |
87 |
88 | /**
89 | * Resizes a given canvas's raster to match its display size.
90 | *
91 | */
92 | Namespace.UiUtils.RightsizeCanvas = function(canv)
93 | {
94 | const clientWidth = Math.round(canv.clientWidth);
95 | const clientHeight = Math.round(canv.clientHeight);
96 | if ( (clientWidth < 1) || (clientHeight < 1) ) { return false; }
97 |
98 | const dpr = 1.33125007; //window.devicePixelRatio || 1;
99 | const requiredCanvasWidth = Math.round(dpr * clientWidth);
100 | const requiredCanvasHeight = Math.round(dpr * clientHeight);
101 |
102 | if ( (canv.width != requiredCanvasWidth) ||
103 | (canv.height != requiredCanvasHeight) ) {
104 | canv.width = requiredCanvasWidth;
105 | canv.height = requiredCanvasHeight;
106 | //canv.getContext('2d').scale(dpr, dpr);
107 | canv.setAttribute('width', requiredCanvasWidth.toString() + 'px');
108 | canv.setAttribute('height', requiredCanvasHeight.toString() + 'px');
109 | }
110 | return true;
111 | };
112 |
113 |
114 | /**
115 | * Checks whether the canvas size has changed.
116 | *
117 | */
118 | Namespace.UiUtils.NeedToRescale = function(drawParams, context)
119 | {
120 | const dp = drawParams;
121 |
122 | if (!dp || !dp.canvasWidth || !dp.canvasHeight) {
123 | return true;
124 | }
125 | else {
126 | return (context.canvas.width != dp.canvasWidth) ||
127 | (context.canvas.height != dp.canvasHeight);
128 | }
129 | };
130 |
131 |
132 | /**
133 | * Draws a line on a canvas.
134 | *
135 | */
136 | Namespace.UiUtils.DrawLine = function(ctx, xa, ya, xb, yb)
137 | {
138 | ctx.beginPath();
139 | ctx.moveTo(xa, ya);
140 | ctx.lineTo(xb, yb);
141 | ctx.stroke();
142 | };
143 |
144 |
145 | /**
146 | * Draws a sequence of lines on a canvas.
147 | *
148 | */
149 | Namespace.UiUtils.DrawLines = function(ctx, pts)
150 | {
151 | if (!pts || (pts.length < 2)) { return; }
152 |
153 | ctx.beginPath();
154 | ctx.moveTo(pts[0].x, pts[0].y);
155 | for (let i=1; i} weights - The relative weights.
73 | * @returns A random element from seq.
74 | */
75 | weightedChoice(seq, weights)
76 | {
77 | if (!seq || !seq.length) {
78 | return null; // (Apparently, many callers rely on this behavior)
79 | }
80 |
81 | const N = seq.length;
82 | if ( N !== weights.length ){
83 | throw new Error("Incompatible array lengths, in RandGen.weightedChoice");
84 | }
85 |
86 | let csum = 0;
87 | const cumWeights = weights.map((csum = 0, n => csum += n));
88 | const r = this.rng() * cumWeights[N-1];
89 | let idx = N-1;
90 | for (let i=0; i>> 18), 597399067);
163 | h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
164 | h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
165 | h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
166 | return [(h1^h2^h3^h4)>>>0, (h2^h1)>>>0, (h3^h1)>>>0, (h4^h1)>>>0];
167 | }
168 |
169 |
170 | /**
171 | * Returns a function that generates random numbers, seeded
172 | * with the given 4-component seed.
173 | * Reference: https://stackoverflow.com/questions/521295/
174 | * @private
175 | *
176 | * @param {Number} a - 1st seed component.
177 | * @param {Number} b - 2nd seed component.
178 | * @param {Number} c - 3rd seed component.
179 | * @param {Number} d - 4th seed component.
180 | * @return {Function} A function that produces random numbers in
181 | * the range [0,1).
182 | */
183 | static _sfc32(a, b, c, d) {
184 | return function() {
185 | a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
186 | let t = (a + b) | 0;
187 | a = b ^ b >>> 9;
188 | b = c + (c << 3) | 0;
189 | c = (c << 21 | c >>> 11);
190 | d = d + 1 | 0;
191 | t = t + d | 0;
192 | c = c + t | 0;
193 | return (t >>> 0) / 4294967296;
194 | };
195 | }
196 | };
197 |
198 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Bond.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * A Bond is a relation between Letters and/or Groups in the same string.
9 | * For example, a letter-successorship relation between 'a' and 'b' in the string 'abc'.
10 | */
11 | Namespace.Bond = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {WorkspaceObject} from - The source object of the bond.
17 | * @param {WorkspaceObject} to - The destination object of the bond.
18 | * @param {SlipNode} category - The bond category (successor, predecessor, or sameness).
19 | * @param {SlipNode} facet - The facet of the bond category (letterCategory or length).
20 | * @param {SlipNode} fromDescriptor - The facet's value for the source object, e.g. 'a'.
21 | * @param {SlipNode} toDescriptor - The facet's value for the destination object, e.g. 'b'.
22 | */
23 | constructor(from, to, category, facet, fromDescriptor, toDescriptor)
24 | {
25 | // WorkspaceStructure members
26 | this.wksp = from.wksp;
27 | this.string = from.string;
28 | this.totalStrength = 0;
29 |
30 | this.source = from;
31 | this.destination = to;
32 | this.category = category;
33 | this.facet = facet;
34 | this.sourceDescriptor = fromDescriptor;
35 | this.destDescriptor = toDescriptor;
36 |
37 | this.leftObject = this.source;
38 | this.rightObject = this.destination;
39 | this.directionCategory = this.wksp.ctx.slipnet.right;
40 | if (this.source.leftIndex > this.destination.rightIndex) {
41 | this.leftObject = this.destination;
42 | this.rightObject = this.source;
43 | this.directionCategory = this.wksp.ctx.slipnet.left;
44 | }
45 | if (fromDescriptor == toDescriptor){
46 | this.directionCategory = null;
47 | }
48 | }
49 |
50 |
51 | /**
52 | * Returns a string describing the object.
53 | */
54 | synopsis(type)
55 | {
56 | const s = this.source.synopsis(1) + ' to ' +
57 | this.destination.synopsis(1) + ` (${this.category.name}, ` +
58 | `${this.facet.name}, ` + `${this.sourceDescriptor.name}, ` +
59 | `${this.destDescriptor.name})`;
60 |
61 | return !type ? s : '';
62 | }
63 |
64 |
65 | /**
66 | * Adds the Bond to the workspace, the parent string, and the
67 | * source and destination objects. Activates its category and
68 | * directionCategory concepts.
69 | *
70 | */
71 | build()
72 | {
73 | this.wksp.structures.push(this);
74 | this.string.bonds.push(this);
75 | this.leftObject.bonds.push(this);
76 | this.rightObject.bonds.push(this);
77 | this.leftObject.rightBond = this;
78 | this.rightObject.leftBond = this;
79 |
80 | this.category.activation = 100;
81 | if (this.directionCategory) {
82 | this.directionCategory.activation = 100;
83 | }
84 | }
85 |
86 |
87 | /**
88 | * Removes the Bond from the workspace, the parent string, and the
89 | * source and destination objects.
90 | *
91 | */
92 | break()
93 | {
94 | this.wksp.structures = this.wksp.structures.filter(s => s !== this);
95 | this.string.bonds = this.string.bonds.filter(s => s !== this);
96 | this.leftObject.bonds = this.leftObject.bonds.filter(s => s !== this);
97 | this.rightObject.bonds = this.rightObject.bonds.filter(s => s !== this);
98 | this.leftObject.rightBond = null;
99 | this.rightObject.leftBond = null;
100 | }
101 |
102 |
103 | /**
104 | * Creates a Bond like this one, except that the source and
105 | * destination are swapped.
106 | */
107 | flippedVersion()
108 | {
109 | return new Namespace.Bond(
110 | this.destination, this.source,
111 | this.category.getRelatedNode(this.wksp.ctx.slipnet.opposite),
112 | this.facet, this.destDescriptor, this.sourceDescriptor
113 | );
114 | }
115 |
116 |
117 | /**
118 | * Updates the total strength value.
119 | *
120 | */
121 | updateStrength()
122 | {
123 | // Calculate the internal strength.
124 | // (Bonds between objects of same type (ie. letter or group) are stronger than
125 | // bonds between different types, and letter bonds are stronger than length bonds.)
126 | const compat = (this.source instanceof Namespace.Letter) == (this.destination instanceof Namespace.Letter) ? 1.0 : 0.7;
127 | const facetFactor = (this.facet == this.wksp.ctx.slipnet.letterCategory) ? 1.0 : 0.7;
128 | let internalStrength = Math.min(100, compat * facetFactor * this.category.bondDegreeOfAssociation() );
129 |
130 | // External strength:
131 | let externalStrength = 0;
132 | const supports = this.string.bonds.map(b => (b.string == this.source.string) &&
133 | (this.leftObject.letterDistance(b.leftObject) !== 0) &&
134 | (this.rightObject.letterDistance(b.rightObject) !== 0) &&
135 | (this.category == b.category) &&
136 | (this.directionCategory == b.directionCategory) ? 1 : 0);
137 |
138 | const nsupporters = supports.reduce((a, b) => a + b, 0);
139 | if (nsupporters > 0) {
140 | const density = 100 * Math.sqrt(this._localDensity());
141 | let supportFactor = Math.pow(0.6, (1/Math.pow(nsupporters,3)));
142 | supportFactor = Math.max(1.0, supportFactor);
143 | externalStrength = supportFactor * density;
144 | }
145 |
146 | // Total strength:
147 | const wti = internalStrength / 100;
148 | const wte = 1 - wti;
149 | this.totalStrength = wti*internalStrength + wte*externalStrength;
150 | }
151 |
152 |
153 | /**
154 | * Returns a measure of the density in the workspace strings of
155 | * bonds with the same bond-category and same direction-category
156 | * as this bond.
157 | *
158 | * @private
159 | */
160 | _localDensity()
161 | {
162 | let slotSum = 0;
163 | let supportSum = 0;
164 |
165 | for (let obj1 of this.wksp.objects.filter(o => o.string == this.string)) {
166 | for (let obj2 of this.wksp.objects.filter(o2 => obj1.isBeside(o2)) ) {
167 | slotSum += 1;
168 | for (const b of this.string.bonds.filter(b => b != this)) {
169 | const sameCats = (b.category == this.category) && (b.directionCategory == this.directionCategory);
170 | const sameEnds = ((this.source == obj1) && (this.destination == obj2)) || ((this.source == obj2) && (this.destination == obj1));
171 | if (sameCats && sameEnds) { supportSum += 1; }
172 | }
173 | }
174 | }
175 | return slotSum === 0 ? 0 : supportSum/slotSum;
176 | }
177 |
178 | };
179 |
180 |
181 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/Codelets/CorrespondenceBuilder.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This codelet tries to build a proposed correspondence, fighting against
10 | * competitors if necessary.
11 | *
12 | */
13 | Namespace.Codelets.CorrespondenceBuilder = class extends Namespace.Codelets.CodeletBase
14 | {
15 | /**
16 | * @constructor
17 | *
18 | * @param {Copycat} ctx - The Copycat instance.
19 | * @param {Number} urgency - The urgency of the codelet.
20 | * @param {Array} args - Arguments to pass to the codelet.
21 | * @param {Number} birthdate - The birthdate of the codelet.
22 | */
23 | constructor(ctx, urgency, args, birthdate)
24 | {
25 | super('correspondence-builder', ctx, urgency, birthdate);
26 | this.correspondence = args[0];
27 | }
28 |
29 |
30 | /**
31 | * Runs the codelet.
32 | */
33 | run()
34 | {
35 | const ctx = this.ctx;
36 | const wksp = ctx.workspace;
37 | const SvS = Namespace.Codelets.CodeletUtils.structureVsStructure;
38 |
39 | const corresp = this.correspondence;
40 | const objFromInitial = corresp.objFromInitial;
41 | const objFromTarget = corresp.objFromTarget;
42 |
43 | // If either of the two objects (or possibly a flipped version) no longer exists, then fizzle.
44 | const wantFlip = corresp.flipTargetObject;
45 | const flippedTargetObj = wantFlip ? objFromTarget.flippedVersion() : null;
46 | const objsExist = wksp.objects.includes(objFromInitial) && (wksp.objects.includes(objFromTarget) ||
47 | (wantFlip && wksp.targetWString.getEquivalentGroup(flippedTargetObj)));
48 | if (!objsExist) { return; }
49 |
50 | // If the correspondence already exists, activate its concept mapping labels,
51 | // and add relevant new ones, and fizzle.
52 | if (this._isReflexive(corresp))
53 | {
54 | const existing = objFromInitial.correspondence;
55 | for (let mapping of corresp.conceptMappings) {
56 | if (mapping.label) {
57 | mapping.label.activation = 100;
58 | }
59 | if (!mapping.isContainedIn(existing.conceptMappings)) {
60 | existing.conceptMappings.push(mapping);
61 | }
62 | }
63 | return;
64 | }
65 |
66 | // Fight incompatibles.
67 | // The weights for the fight depend on the letter-span of the objects.
68 | // This is one of the reasons the program prefers correspondences to groups rather than to letters.
69 | // Another reason is that groups are more salient than letters, so they are more likely to be chosen by correspondence scouts.
70 | const incompatibles = wksp.initialWString.objects.filter(o =>
71 | o.correspondence && corresp.isIncompatibleWith(o.correspondence)).map(o => o.correspondence);
72 | if (incompatibles.length) {
73 | const correspSpan = corresp.objFromInitial.letterSpan() + corresp.objFromTarget.letterSpan();
74 | for (let incompat of incompatibles) {
75 | const incompatSpan = incompat.objFromInitial.letterSpan() + incompat.objFromTarget.letterSpan();
76 | if (!SvS(corresp, correspSpan, incompat, incompatSpan)) {
77 | return;
78 | }
79 | }
80 | }
81 |
82 | // If there is an incompatible bond, then fight against it, and its group, if any.
83 | let incompatibleBond;
84 | let incompatibleGroup;
85 | if ((objFromInitial.leftmost || objFromInitial.rightmost) && (objFromTarget.leftmost || objFromTarget.rightmost))
86 | {
87 | incompatibleBond = this._getIncompatibleBond(corresp);
88 | if (incompatibleBond) {
89 | if (!SvS(corresp, 3, incompatibleBond, 2)) {
90 | return;
91 | }
92 | incompatibleGroup = objFromTarget.group;
93 | if (incompatibleGroup) {
94 | if (!SvS(corresp, 3, incompatibleGroup, 2)) {
95 | return;
96 | }
97 | }
98 | }
99 | }
100 |
101 | // If there is an incompatible rule, fight against it
102 | let incompatibleRule;
103 | if (wksp.rule && this._incompatibleRuleCorrespondence(wksp.rule, corresp)) {
104 | incompatibleRule = wksp.rule;
105 | if (!SvS(corresp, 1, incompatibleRule, 1)) {
106 | return;
107 | }
108 | }
109 |
110 | incompatibles.forEach(x => x.break());
111 | if (incompatibleBond) {
112 | incompatibleBond.break();
113 | }
114 | if (incompatibleGroup) {
115 | incompatibleGroup.break();
116 | }
117 | if (incompatibleRule) {
118 | incompatibleRule.break();
119 | }
120 | corresp.build();
121 | }
122 |
123 |
124 | /**
125 | * Determines if the given rule and correspondence are incompatible.
126 | * @private
127 | *
128 | */
129 | _incompatibleRuleCorrespondence(rule, corresp)
130 | {
131 | if (!rule || !corresp) {
132 | return false;
133 | }
134 |
135 | // Find changed object
136 | const changeds = this.ctx.workspace.initialWString.objects.filter(o => o.changed);
137 | if (!changeds.length) {
138 | return false;
139 | }
140 |
141 | const changed = changeds[0];
142 | if (corresp.objFromInitial != changed) {
143 | return false;
144 | }
145 |
146 | // It is incompatible if the rule descriptor is not in the mapping list
147 | return corresp.conceptMappings.some(m => m.initialDescriptor == rule.descriptor);
148 | }
149 |
150 |
151 | /**
152 | * Gets the incompatible bond, if any, for the given correspondence.
153 | * @private
154 | *
155 | */
156 | _getIncompatibleBond(corresp)
157 | {
158 | const sn = this.ctx.slipnet;
159 |
160 | const initialBond = corresp.objFromInitial.leftmost ? corresp.objFromInitial.rightBond :
161 | corresp.objFromInitial.rightmost ? corresp.objFromInitial.leftBond : null;
162 | if (!initialBond ) {
163 | return null;
164 | }
165 |
166 | const targetBond = corresp.objFromTarget.leftmost ? corresp.objFromTarget.rightBond :
167 | corresp.objFromTarget.rightmost ? corresp.objFromTarget.leftBond : null;
168 | if (!targetBond ) {
169 | return null;
170 | }
171 |
172 | if (initialBond.directionCategory && targetBond.directionCategory) {
173 | const mapping = new Namespace.ConceptMapping(sn.directionCategory, sn.directionCategory,
174 | initialBond.directionCategory, targetBond.directionCategory, null, null);
175 | if (corresp.conceptMappings.some(m => m.isIncompatibleWith(mapping))) {
176 | return targetBond;
177 | }
178 | }
179 | return null;
180 | }
181 |
182 |
183 | /**
184 | * Determines if the given correspondence is reflexive.
185 | * @private
186 | *
187 | */
188 | _isReflexive(corresp)
189 | {
190 | const initial = corresp.objFromInitial;
191 | return initial.correspondence && (initial.correspondence.objFromTarget == corresp.objFromTarget);
192 | }
193 |
194 | };
195 |
196 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------
/src/UI/InputUi.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 |
5 |
6 | /**
7 | * @classdesc
8 | * This class draws the input widgets.
9 | *
10 | */
11 | Namespace.InputUi = class {
12 |
13 | /**
14 | * @constructor
15 | *
16 | * @param {CopycatUi} copycatUi - The parent Ui.
17 | * @param {HTMLElement} parentDiv - The html div that hosts
18 | * this ui.
19 | */
20 | constructor(copycatUi, parentDiv)
21 | {
22 | this.copycatUi = copycatUi;
23 | this.parentDiv = parentDiv;
24 | this.copycat = copycatUi.copycat;
25 | this.msgTimerId = null;
26 | this.drawParams = {};
27 |
28 | this.bkgndColor = '#b3ddcc';
29 | this.inputFont = {family:'serif', weight: 'bold', style: 'italic', size: '3.5vmin'};
30 | this.inputFontColor = '#1581e7';
31 | this.answerFontColor = '#d20000';
32 | this.inputDisabledFontColor = '#6eb4f2';
33 | this.msgFontColor = '#d20000';
34 | this.inputBkgndColor = '#dfdfdf';
35 |
36 | this._buildUi();
37 | }
38 |
39 |
40 | /**
41 | * Creates the ui elements.
42 | * @private
43 | *
44 | */
45 | _buildUi()
46 | {
47 | const UiUtils = Namespace.UiUtils;
48 |
49 | // Create the main div
50 | this.mainDiv = UiUtils.CreateElement('div',
51 | 'input-div', this.parentDiv,
52 | {top:'0%', left:'0%', width:'100%', height:'100%',
53 | border:'1px solid black', borderLeft:'none',
54 | background:this.bkgndColor});
55 |
56 | // Create the text-input elements
57 | const wa = this.initialStringInput = UiUtils.CreateElement('input',
58 | 'initial-string-input', this.mainDiv, {left:'3%'}, {type:'text'});
59 | wa.onkeyup = this._onKeyup.bind(this);
60 |
61 | const wb = this.modifiedStringInput = UiUtils.CreateElement('input',
62 | 'modified-string-input', this.mainDiv, {left:'27%'}, {type:'text'});
63 | wb.onkeyup = this._onKeyup.bind(this);
64 |
65 | const wc = this.targetStringInput = UiUtils.CreateElement('input',
66 | 'target-string-input', this.mainDiv, {left:'55%'}, {type:'text'});
67 | wc.onkeyup = this._onKeyup.bind(this);
68 |
69 | const wd = this.answerStringInput = UiUtils.CreateElement('input',
70 | 'answer-string-input', this.mainDiv, {left:'79%'}, {type:'text'});
71 | wd.readOnly = true;
72 | wd.className += " noselect";
73 |
74 | // Configure the text-input elements
75 | const font = this.inputFont;
76 | for (let w of [wa, wb, wc, wd]) {
77 | UiUtils.StyleElement(w, {top:'28%', width:'18%', height:'44%',
78 | textAlign:'center', border:'1px solid gray',
79 | fontFamily: font.family, fontWeight: font.weight,
80 | fontStyle: font.style, fontSize: font.size,
81 | color: (w == wd) ? this.answerFontColor : this.inputFontColor,
82 | background:this.inputBkgndColor});
83 | w.setAttribute('spellcheck', 'false');
84 | }
85 | const wksp = this.copycat.workspace;
86 | this.initialStringInput.value = wksp.initialWString.jstring;
87 | this.modifiedStringInput.value = wksp.modifiedWString.jstring;
88 | this.targetStringInput.value = wksp.targetWString.jstring;
89 | this.answerStringInput.value = '?';
90 |
91 | // Arrpws
92 | this.arrowSpan1 = UiUtils.CreateElement('span',
93 | 'arrow-span1', this.mainDiv,
94 | {top:'28%', left:'21%', width:'6%', height:'44%', display:'flex',
95 | alignItems:'center', justifyContent:'center',
96 | fontWeight: font.weight, fontStyle: font.style,
97 | fontSize: font.size},
98 | {innerHTML:'→'});
99 | this.arrowSpan1.className += " noselect";
100 |
101 | this.arrowSpan2 = UiUtils.CreateElement('span',
102 | 'arrow-span2', this.mainDiv,
103 | {top:'28%', left:'73%', width:'6%', height:'44%', display:'flex',
104 | alignItems:'center', justifyContent:'center',
105 | fontWeight: font.weight, fontStyle: font.style,
106 | fontSize: font.size},
107 | {innerHTML:'→'});
108 | this.arrowSpan2.className += " noselect";
109 |
110 | // Message area
111 | this.messageDiv = UiUtils.CreateElement('div',
112 | 'message-div', this.mainDiv,
113 | {top:'74%', left:'0%', width:'100%', height:'24%', display:'flex',
114 | alignItems:'center', justifyContent:'center',
115 | fontWeight: font.weight, fontStyle: font.style,
116 | fontSize: '3vmin', color:this.msgFontColor});
117 | this.messageDiv.className += " noselect";
118 |
119 | // Colon separator
120 | this.colonDiv = UiUtils.CreateElement('div',
121 | 'colon-div', this.mainDiv,
122 | {top:'0', left:'45%', width:'10%', height:'100%', display:'flex',
123 | alignItems:'center', justifyContent:'center',
124 | fontWeight: font.weight,
125 | fontSize: '6vmin', color:'#606060'});
126 | this.colonDiv.innerHTML = ': :';
127 | this.colonDiv.className += " noselect";
128 | }
129 |
130 |
131 | /**
132 | * Returns the strings that are currently entered in the
133 | * input fields.
134 | *
135 | */
136 | getInputStrings()
137 | {
138 | const rawStrings = [this.initialStringInput.value, this.modifiedStringInput.value, this.targetStringInput.value];
139 | const normStrings = rawStrings.map(s => s.trim().toLowerCase());
140 |
141 | this.initialStringInput.value = normStrings[0];
142 | this.modifiedStringInput.value = normStrings[1];
143 | this.targetStringInput.value = normStrings[2];
144 |
145 | return normStrings;
146 | }
147 |
148 |
149 | /**
150 | * Displays a message beneath the input widgets.
151 | *
152 | */
153 | displayMessage(msg)
154 | {
155 | this.messageDiv.innerHTML = msg;
156 | if (this.msgTimerId !== null) {
157 | clearTimeout(this.msgTimerId);
158 | }
159 | this.msgTimerId = setTimeout(() => {
160 | this.messageDiv.innerHTML = '';
161 | this.msgTimerId = null;
162 | }, 1800);
163 | }
164 |
165 |
166 | /**
167 | * Handler for resize events.
168 | * @private
169 | *
170 | */
171 | _onResize()
172 | {
173 | // Nothing to do here.
174 | }
175 |
176 |
177 | /**
178 | * Handler for state change events.
179 | * @private
180 | *
181 | */
182 | _onCopycatStateChange()
183 | {
184 | const copycat = this.copycat;
185 | const ans = copycat.workspace.finalAnswer;
186 | this.answerStringInput.value = (copycat.batchMode) ? '?' : ans || '?';
187 | this._setInputsEnabled(copycat.state != 'running');
188 | }
189 |
190 |
191 | /**
192 | * Handler for key-up events.
193 | * @private
194 | *
195 | */
196 | _onKeyup(e)
197 | {
198 | if (e.key === 'Enter' || e.keyCode === 13) {
199 | this.copycatUi.workspaceHeaderUi._onResetBtnClick();
200 | }
201 | }
202 |
203 |
204 | /**
205 | * Enables or disables the input fields.
206 | *
207 | * @private
208 | */
209 | _setInputsEnabled(enabled)
210 | {
211 | this.initialStringInput.disabled = !enabled;
212 | this.modifiedStringInput.disabled = !enabled;
213 | this.targetStringInput.disabled = !enabled;
214 |
215 | this.initialStringInput.style.color = enabled ? this.inputFontColor : this.inputDisabledFontColor;
216 | this.modifiedStringInput.style.color = enabled ? this.inputFontColor : this.inputDisabledFontColor;
217 | this.targetStringInput.style.color = enabled ? this.inputFontColor : this.inputDisabledFontColor;
218 | }
219 | };
220 |
221 |
222 | })( window.CopycatJS = window.CopycatJS || {} );
223 |
--------------------------------------------------------------------------------
/src/Codelets/CodeletUtils.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-shadow-restricted-names, no-unused-vars, no-extra-semi
2 | ;(function(Namespace, undefined) {
3 | "use strict";
4 | Namespace.Codelets = Namespace.Codelets || {};
5 |
6 |
7 | /**
8 | * @classdesc
9 | * This class provides several utility functions
10 | * needed by the codelets.
11 | *
12 | */
13 | Namespace.Codelets.CodeletUtils = class
14 | {
15 | /**
16 | * Returns a quantized measure of urgency.
17 | *
18 | * @param {Number} urgency - The input urgency.
19 | */
20 | static getUrgencyBin(urgency)
21 | {
22 | const numUrgencyBins = 7;
23 | const i = Math.floor(urgency) * numUrgencyBins / 100;
24 | return (i >= numUrgencyBins) ? numUrgencyBins : i + 1;
25 | }
26 |
27 |
28 | /**
29 | * Returns a random object from a given list, that is
30 | * not from the modified string. The probability of
31 | * choosing an object is weighted by its value of the
32 | * given attribute.
33 | *
34 | * @param {Copycat} ctx - The Copycat instance.
35 | * @param {String} attribute - The attribute to use for weighting.
36 | * @param {Array} inObjects - The list of objects to choose from.
37 | *
38 | */
39 | static chooseUnmodifiedObject(ctx, attribute, inObjects)
40 | {
41 | const candidates = inObjects.filter(o => o.string != ctx.workspace.modifiedWString);
42 | const weights = candidates.map( o => ctx.temperature.getAdjustedValue(o[attribute]) );
43 | return ctx.randGen.weightedChoice(candidates, weights);
44 | }
45 |
46 |
47 | /**
48 | * Returns a random nearest-neighbor of a given object.
49 | *
50 | * @param {Copycat} ctx - The Copycat instance.
51 | * @param {WorkspaceObject} sourceObj - The input object.
52 | *
53 | */
54 | static chooseNeighbor(ctx, sourceObj)
55 | {
56 | const candidates = ctx.workspace.objects.filter(o => o.isBeside(sourceObj));
57 | const weights = candidates.map( o => ctx.temperature.getAdjustedValue(o.intraStringSalience) );
58 | return ctx.randGen.weightedChoice(candidates, weights);
59 | }
60 |
61 |
62 | /**
63 | * Randomly selects an object from the initial or target string that
64 | * matches the given criterion.
65 | *
66 | * @param {Copycat} ctx - The Copycat instance.
67 | * @param {String} relevanceCriterion - 'bondCategory' or 'bondDirection'.
68 | * @param {SlipNode} toMatch - The SlipNode to match.
69 | */
70 | static getScoutSource(ctx, relevanceCriterion, toMatch)
71 | {
72 | // Define a function to compute the relevance of a string.
73 | const relevance = function(string, criterion, nodeToMatch)
74 | {
75 | if ((criterion == 'bondCategory') && (string.objects.length == 1)) {
76 | return 0;
77 | }
78 |
79 | const nonSpanningObjs = string.objects.filter(o => !o.spansString());
80 | const numNonSpanningObjects = nonSpanningObjs.length;
81 | let numMatches = 0;
82 | if (criterion == 'bondCategory') {
83 | numMatches = nonSpanningObjs.filter(o => o.rightBond && (o.rightBond.category == nodeToMatch)).length;
84 | }
85 | else if (criterion == 'bondDirection') {
86 | numMatches = nonSpanningObjs.filter(o => o.rightBond && (o.rightBond.directionCategory == nodeToMatch)).length;
87 | }
88 |
89 | return (numNonSpanningObjects == 1) ? 100 * numMatches : (100 * numMatches) / (numNonSpanningObjects - 1);
90 | };
91 |
92 |
93 | // Get relevance and unhappiness values.
94 | const wksp = ctx.workspace;
95 | const initialRelevance = relevance(wksp.initialWString, relevanceCriterion, toMatch);
96 | const targetRelevance = relevance(wksp.targetWString, relevanceCriterion, toMatch);
97 | const initialUnhappiness = wksp.initialWString.intraStringUnhappiness;
98 | const targetUnhappiness = wksp.targetWString.intraStringUnhappiness;
99 | const initials = initialRelevance + initialUnhappiness;
100 | const targets = targetRelevance + targetUnhappiness;
101 |
102 | // Choose a source object.
103 | let result;
104 | const Utils = Namespace.Codelets.CodeletUtils;
105 | if (ctx.randGen.weightedGreaterThan(targets, initials)) {
106 | result = Utils.chooseUnmodifiedObject(ctx, 'intraStringSalience', wksp.targetWString.objects);
107 | }else {
108 | result = Utils.chooseUnmodifiedObject(ctx, 'intraStringSalience', wksp.initialWString.objects);
109 | }
110 |
111 | return result;
112 | }
113 |
114 |
115 | /**
116 | * Randomly selects a bond facet that is common to the
117 | * given source and destination objects.
118 | *
119 | * @param {Copycat} ctx - The Copycat instance.
120 | * @param {WorkspaceObject} sourceObj - The source object.
121 | * @param {WorkspaceObject} destObj - The destination object.
122 | *
123 | */
124 | static chooseBondFacet(ctx, sourceObj, destObj)
125 | {
126 | // The allowed bond facets:
127 | const bondFacets = [ctx.slipnet.letterCategory, ctx.slipnet.length];
128 |
129 | // Get the bond facets that are present in both source and destination.
130 | const sourceFacets = sourceObj.descriptions.map(d => d.descriptionType).filter(d => bondFacets.includes(d));
131 |
132 | const candidateFacets = destObj.descriptions.map(d => d.descriptionType).filter(d => sourceFacets.includes(d));
133 |
134 | // For each such facet, compute a support value based on both the facet's activation
135 | // and the number of objects in the source's string that share the facet.
136 | const facetWeights = [];
137 | const sourceSiblings = ctx.workspace.objects.filter(o => o.string == sourceObj.string);
138 | const siblingDescriptions = sourceSiblings.map(o => o.descriptions).flat();
139 | for (let facet of candidateFacets) {
140 | let supportCount = 0;
141 | for (let d of siblingDescriptions) {
142 | supportCount += (d.descriptionType == facet) ? 1 : 0;
143 | }
144 | const stringSupport = 100 * supportCount / (sourceSiblings.length || 1);
145 | facetWeights.push( (facet.activation + stringSupport)/2 );
146 | }
147 |
148 | return ctx.randGen.weightedChoice(candidateFacets, facetWeights);
149 | }
150 |
151 |
152 | /**
153 | * Probabilistically decides which of two given structures to return,
154 | * based on their strengths as well as external weight factors.
155 | *
156 | * @param {WorkspaceStructure} structure1 - The first structure.
157 | * @param {Number} weight1 - The weight factor for the first structure.
158 | * @param {WorkspaceStructure} structure2 - The second structure.
159 | * @param {Number} weight2 - The weight factor for the second structure.
160 | *
161 | */
162 | static structureVsStructure(structure1, weight1, structure2, weight2)
163 | {
164 | const ctx = structure1.wksp.ctx;
165 | const temperature = ctx.temperature;
166 |
167 | structure1.updateStrength();
168 | structure2.updateStrength();
169 | const weightedStrength1 = temperature.getAdjustedValue(structure1.totalStrength * weight1);
170 | const weightedStrength2 = temperature.getAdjustedValue(structure2.totalStrength * weight2);
171 |
172 | return ctx.randGen.weightedGreaterThan(weightedStrength1, weightedStrength2);
173 | }
174 |
175 |
176 | /**
177 | * Pits the given structure against the given list of incompatible
178 | * structures, deciding probabilistically on a winner.
179 | *
180 | * @param {WorkspaceStructure} structure - The structure to test.
181 | * @param {Number} structureWeight - The weight factor for the structure.
182 | * @param {Array} incompatibles - The list of incompatible structures.
183 | * @param {Number} incompatiblesWeight - The weight factor for the incompatible structures.
184 | *
185 | */
186 | static fightItOut(structure, structureWeight, incompatibles, incompatiblesWeight)
187 | {
188 | if (!incompatibles || !incompatibles.length) {
189 | return true;
190 | }
191 |
192 | return incompatibles.every(incomp =>
193 | Namespace.Codelets.CodeletUtils.structureVsStructure(structure, structureWeight, incomp, incompatiblesWeight)
194 | );
195 | }
196 |
197 | };
198 |
199 | })( window.CopycatJS = window.CopycatJS || {} );
--------------------------------------------------------------------------------