├── .gitignore ├── LICENSE.md ├── README.md ├── _config.yml ├── dist ├── CopycatJS.js ├── btn_pause.png ├── btn_play.png ├── btn_reset.png ├── btn_step.png ├── cc_logo.png ├── index.html └── styles.css ├── docs └── Slipnet.pdf ├── md_screenshot-1.jpg └── src ├── Bond.js ├── Codelets ├── BondBuilder.js ├── BondStrengthTester.js ├── BottomUpBondScout.js ├── BottomUpCorrespondenceScout.js ├── BottomUpDescriptionScout.js ├── Breaker.js ├── CodeletBase.js ├── CodeletFactory.js ├── CodeletUtils.js ├── CorrespondenceBuilder.js ├── CorrespondenceStrengthTester.js ├── DescriptionBuilder.js ├── DescriptionStrengthTester.js ├── GroupBuilder.js ├── GroupScout_WholeString.js ├── GroupStrengthTester.js ├── ImportantObjectCorrespondenceScout.js ├── ReplacementFinder.js ├── RuleBuilder.js ├── RuleScout.js ├── RuleStrengthTester.js ├── RuleTranslator.js ├── TopDownBondScout_Category.js ├── TopDownBondScout_Direction.js ├── TopDownDescriptionScout.js ├── TopDownGroupScout_Category.js └── TopDownGroupScout_Direction.js ├── Coderack.js ├── ConceptMapping.js ├── ConsoleReporter.js ├── Copycat.js ├── Correspondence.js ├── Description.js ├── Group.js ├── Letter.js ├── RandGen.js ├── Replacement.js ├── Rule.js ├── SlipLink.js ├── SlipNode.js ├── Slipnet.js ├── Temperature.js ├── UI ├── ArrowGraphic.js ├── BatchmodeUi.js ├── BondsGraphic.js ├── CoderackUi.js ├── CopycatUi.js ├── CorrsGraphic.js ├── DescriptionsGraphic.js ├── Dialog.js ├── Flasher.js ├── GroupsGraphic.js ├── HelpDialog.js ├── InputUi.js ├── LettersGraphic.js ├── ReplacementsGraphic.js ├── RuleGraphic.js ├── SlipnetUi.js ├── StringGraphic.js ├── TopbarUi.js ├── UiUtils.js ├── WorkspaceHeaderUi.js ├── WorkspaceUi.js ├── btn_pause.png ├── btn_play.png ├── btn_reset.png ├── btn_step.png ├── cc_logo.png └── styles.css ├── Workspace.js ├── WorkspaceObject.js ├── WorkspaceString.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paul Geiger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # copycat-js 2 | 3 | [](https://paul-g2.github.io/copycat-js/) 4 | 5 |
6 | 7 | This is a javascript version of the [Copycat](https://en.wikipedia.org/wiki/Copycat_(software)) program 8 | developed by [Douglas Hofstadter](https://cogs.indiana.edu/directory/faculty/profile.php?faculty=dughof) 9 | and [Melanie Mitchell](https://melaniemitchell.me). 10 | 11 | [You can try it out here.](https://paul-g2.github.io/copycat-js/) 12 | 13 | Copycat is a computer model of human analogy-making. 14 | It tries to solve letter puzzles of the form "abc is to abd as ijk is to what?" 15 | 16 | It's internal workings are described in great detail in the books 17 | [Fluid Concepts and Creative Analogies](https://www.amazon.com/Fluid-Concepts-Creative-Analogies-Fundamental/dp/0465024750), and 18 | [Analogy-Making as Perception](https://www.amazon.com/Analogy-Making-Perception-Computer-Modeling-Connectionism/dp/026251544X). 19 | 20 | I got interested in Copycat after reading the Fluid Concepts and Creative Analogies book, 21 | but I couldn't find a demo that was easy to install and use ... hence this project. 22 | 23 | The [original Copycat code](https://github.com/fargonauts/copycat-lisp) was written in lisp, 24 | which I am not fluent in, so I based this port 25 | (except for the UI) mainly on the [python version by Lucas Saldyt](https://github.com/fargonauts/copycat), 26 | occasionaly referring to the lisp version when something was unclear. 27 | 28 | The look of the UI is based on that of [James Marshall's Metacat](http://science.slc.edu/jmarshall/metacat/) 29 | (which is an extension of Copycat). 30 | 31 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /dist/btn_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/dist/btn_pause.png -------------------------------------------------------------------------------- /dist/btn_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/dist/btn_play.png -------------------------------------------------------------------------------- /dist/btn_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/dist/btn_reset.png -------------------------------------------------------------------------------- /dist/btn_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/dist/btn_step.png -------------------------------------------------------------------------------- /dist/cc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/dist/cc_logo.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copycat 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /dist/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #000000; 5 | } 6 | div#frame_area { 7 | position: relative; 8 | width: 100vw; 9 | height: 100vh; 10 | height: 100dvh; 11 | margin: 0; 12 | padding: 0; 13 | overflow: hidden; 14 | background: #7092BE; 15 | } 16 | div#app_area { 17 | margin: 0; 18 | padding: 0; 19 | position: absolute; 20 | top: 0px; 21 | right: 0px; 22 | bottom: 0px; 23 | left: 0px; 24 | background: #e0d20b; 25 | } 26 | div#topbar_area { 27 | margin: 0; 28 | padding: 0; 29 | position: absolute; 30 | top: 0px; 31 | height: 7%; 32 | left: 0px; 33 | right: 0px; 34 | background: #bfcbdf; 35 | } 36 | div#widgets_area { 37 | margin: 0; 38 | padding: 0; 39 | position: absolute; 40 | top: 7%; 41 | height: 93%; 42 | left: 0px; 43 | right: 0px; 44 | background: #a870be; 45 | } 46 | div#input_area { 47 | margin: 0; 48 | padding: 0; 49 | position: absolute; 50 | top: 0px; 51 | height: 15%; 52 | left: 0px; 53 | width: 60%; 54 | background: #b3ddcc; 55 | } 56 | div#workspace_header_area { 57 | margin: 0; 58 | padding: 0; 59 | position: absolute; 60 | top: 15%; 61 | height: 12%; 62 | left: 0px; 63 | width: 60%; 64 | background: #eeeeee; 65 | } 66 | div#workspace_area { 67 | margin: 0; 68 | padding: 0; 69 | position: absolute; 70 | top: 27%; 71 | height: 73%; 72 | left: 0px; 73 | width: 60%; 74 | background: #ffffff; 75 | } 76 | div#batchmode_area { 77 | margin: 0; 78 | padding: 0; 79 | position: absolute; 80 | top: 15%; 81 | height: 85%; 82 | left: 0px; 83 | width: 100%; 84 | background: #ffffff; 85 | display: none; 86 | } 87 | div#slipnet_area { 88 | margin: 0; 89 | padding: 0; 90 | position: absolute; 91 | top: 0px; 92 | bottom: 0px; 93 | left: 60%; 94 | width: 25%; 95 | background: #fffbcc; 96 | } 97 | div#coderack_area { 98 | margin: 0; 99 | padding: 0; 100 | position: absolute; 101 | top: 0px; 102 | bottom: 0px; 103 | left: 85%; 104 | width: 15%; 105 | background: #ffe5e0; 106 | } 107 | .noselect { 108 | -webkit-touch-callout: none; 109 | -webkit-user-select: none; 110 | -khtml-user-select: none; 111 | -moz-user-select: none; 112 | -ms-user-select: none; 113 | user-select: none; 114 | } 115 | p + ul { 116 | margin-top: -16px; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /docs/Slipnet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/docs/Slipnet.pdf -------------------------------------------------------------------------------- /md_screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/md_screenshot-1.jpg -------------------------------------------------------------------------------- /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/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/Codelets/BondStrengthTester.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 tests the strength of a bond, and if it's strong enough, 9 | * may post a BondBuilder codelet. 10 | * 11 | */ 12 | Namespace.Codelets.BondStrengthTester = 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('bond-strength-tester', ctx, urgency, birthdate); 25 | this.bond = args[0]; 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const ctx = this.ctx; 35 | const coderack = ctx.coderack; 36 | const bond = this.bond; 37 | 38 | // Provide UI feedback 39 | if (ctx.ui && !ctx.batchMode) { 40 | ctx.ui.workspaceUi.getStringGraphic(bond.string).bondsGraphic.flashProposed(bond); 41 | } 42 | 43 | // Maybe fizzle, if the strength is too low 44 | bond.updateStrength(); 45 | const strength = bond.totalStrength; 46 | const prob = ctx.temperature.getAdjustedProb(strength/100); 47 | if ( !ctx.randGen.coinFlip(prob) ) { 48 | return; 49 | } 50 | 51 | // Post a BondBuilder codelet 52 | bond.facet.activation = 100; 53 | bond.sourceDescriptor.activation = 100; 54 | bond.destDescriptor.activation = 100; 55 | const urgency = Namespace.Codelets.CodeletUtils.getUrgencyBin(strength); 56 | const newCodelet = ctx.coderack.factory.create('bond-builder', urgency, [bond]); 57 | coderack.post(newCodelet); 58 | } 59 | 60 | }; 61 | 62 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/BottomUpBondScout.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 | const CodeletUtils = Namespace.Codelets.CodeletUtils; 6 | 7 | /** 8 | * @classdesc 9 | * This codelet looks for potential bonds between 10 | * neighboring object pairs in the initial or target string. 11 | * 12 | */ 13 | Namespace.Codelets.BottomUpBondScout = 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('bottom-up-bond-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 | 37 | // Choose a workspace object at random, based on intra-string salience. 38 | const bondSource = CodeletUtils.chooseUnmodifiedObject(ctx, 'intraStringSalience', ctx.workspace.objects); 39 | if (!bondSource) { return; } 40 | 41 | // Choose a neighboring object 42 | const bondDest = CodeletUtils.chooseNeighbor(ctx, bondSource); 43 | if (!bondDest) { return; } 44 | 45 | // Provide UI feedback. 46 | if (ctx.ui && !ctx.batchMode) { 47 | const dummyBond = new Namespace.Bond(bondSource, bondDest, sn.sameness, sn.letterCategory, sn.letters[0], sn.letters[0]); 48 | ctx.ui.workspaceUi.getStringGraphic(dummyBond.string).bondsGraphic.flashGrope(dummyBond); 49 | } 50 | 51 | // Choose a bond facet 52 | const bondFacet = CodeletUtils.chooseBondFacet(ctx, bondSource, bondDest); 53 | if (!bondFacet) { return; } 54 | 55 | // Get the bond category 56 | const sourceDescriptor = bondSource.getDescriptor(bondFacet); 57 | const destDescriptor = bondDest.getDescriptor(bondFacet); 58 | let bondCategory = sourceDescriptor.getBondCategory(destDescriptor); 59 | if (!bondCategory) { 60 | return; 61 | } 62 | if (bondCategory == ctx.slipnet.identity) { 63 | bondCategory = ctx.slipnet.sameness; 64 | } 65 | 66 | // Propose the bond 67 | ctx.coderack.proposeBond(bondSource, bondDest, bondCategory, bondFacet, sourceDescriptor, destDescriptor); 68 | } 69 | 70 | }; 71 | 72 | })( 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/BottomUpDescriptionScout.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 | * 9 | * Thic codelet seeks an appropriate Description for a randomly 10 | * selected workspace object. 11 | */ 12 | Namespace.Codelets.BottomUpDescriptionScout = 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-description-scout', ctx, urgency, birthdate); 25 | } 26 | 27 | 28 | /** 29 | * Runs the codelet. 30 | */ 31 | run() 32 | { 33 | const ctx = this.ctx; 34 | const Utils = Namespace.Codelets.CodeletUtils; 35 | 36 | // Choose a workspace object, based on salience. 37 | const chosenObject = Utils.chooseUnmodifiedObject(ctx, 'totalSalience', ctx.workspace.objects); 38 | if (!chosenObject) { return; } 39 | 40 | // Choose a relevant description by activation 41 | const descriptions = chosenObject.relevantDescriptions(); 42 | const description = ctx.randGen.weightedChoice(descriptions, descriptions.map(d => d.activation)); 43 | if (!description) { return; } 44 | 45 | // Choose one of the description's property links 46 | const propertyLinks = this._shortPropertyLinks(ctx, description.descriptor); 47 | if (!propertyLinks.length) { return; } 48 | 49 | const sWeights = propertyLinks.map(s => s.degreeOfAssociation() * s.destination.activation); 50 | const chosen = ctx.randGen.weightedChoice(propertyLinks, sWeights); 51 | const chosenProperty = chosen.destination; 52 | 53 | // Propose the description 54 | ctx.coderack.proposeDescription(chosenObject, chosenProperty.category(), chosenProperty); 55 | } 56 | 57 | 58 | 59 | /** 60 | * Returns a random subset a descriptor's property links, preferring 61 | * links that are short. 62 | * @private 63 | * 64 | */ 65 | _shortPropertyLinks(ctx, descriptor) 66 | { 67 | const result = []; 68 | for (let propertyLink of descriptor.propertyLinks) 69 | { 70 | const association = propertyLink.degreeOfAssociation() / 100; 71 | const prob = ctx.temperature.getAdjustedProb(association); 72 | if (ctx.randGen.coinFlip(prob)) { result.push(propertyLink); } 73 | } 74 | return result; 75 | } 76 | 77 | }; 78 | 79 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/Breaker.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 break a randomly selected bond, 10 | * group, or correspondence in the workspace. 11 | * 12 | */ 13 | Namespace.Codelets.Breaker = 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('breaker', ctx, urgency, birthdate); 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const randGen = this.ctx.randGen; 35 | const temperature = this.ctx.temperature; 36 | 37 | // Maybe fizzle 38 | const fizzleProb = (1 - temperature.value()/100); 39 | if (randGen.coinFlip(fizzleProb)) { return; } 40 | 41 | // Choose a Group or Bond or Correspondence at random 42 | const structures = this.ctx.workspace.structures.filter(s => 43 | (s instanceof Namespace.Group) || (s instanceof Namespace.Bond) || (s instanceof Namespace.Correspondence)); 44 | if (!structures.length) { return; } 45 | 46 | const structure = randGen.choice(structures); 47 | const breakObjects = [structure]; 48 | if (structure instanceof Namespace.Bond) { 49 | if (structure.source.group && (structure.source.group == structure.destination.group)) { 50 | breakObjects.push(structure.source.group); 51 | } 52 | } 53 | 54 | // Break the bond(s) 55 | for (let structure of breakObjects) { 56 | const breakProb = temperature.getAdjustedProb(structure.totalStrength/100); 57 | if (randGen.coinFlip(breakProb)) { return; } 58 | } 59 | breakObjects.forEach( (structure) => structure.break() ); 60 | } 61 | }; 62 | 63 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/CodeletBase.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 is the base class for all codelets. 10 | * 11 | */ 12 | Namespace.Codelets.CodeletBase = class 13 | { 14 | /** 15 | * @constructor 16 | */ 17 | constructor(name, ctx, urgency, birthdate) 18 | { 19 | this.name = name; 20 | this.ctx = ctx; 21 | this.urgency = urgency; 22 | this.birthdate = birthdate; 23 | } 24 | 25 | 26 | /** 27 | * Returns a string describing the object. 28 | * 29 | */ 30 | synopsis(type) 31 | { 32 | return !type ? this.name : ''; 33 | } 34 | }; 35 | 36 | })( 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/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 || {} ); -------------------------------------------------------------------------------- /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/Codelets/CorrespondenceStrengthTester.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 tests the strength of a correspondence, and if it's 9 | * strong enough, may post a CorrespondenceBuilder codelet. 10 | * 11 | */ 12 | Namespace.Codelets.CorrespondenceStrengthTester = 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('correspondence-strength-tester', ctx, urgency, birthdate); 25 | this.correspondence = 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 coderack = ctx.coderack; 37 | 38 | const corresp = this.correspondence; 39 | const objFromInitial = corresp.objFromInitial; 40 | const objFromTarget = corresp.objFromTarget; 41 | 42 | // If either of the two objects (or possibly a flipped version) no longer 43 | // 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 | // Provide UI feedback 51 | if (ctx.ui && !ctx.batchMode) { 52 | ctx.ui.workspaceUi.corrsGraphic.flashProposed(corresp); 53 | } 54 | 55 | corresp.updateStrength(); 56 | const strength = corresp.totalStrength; 57 | if (ctx.randGen.coinFlip( ctx.temperature.getAdjustedProb(strength/100) )) 58 | { 59 | // Activate the correspondence's mappings 60 | corresp.conceptMappings.forEach(m => { 61 | m.initialDescType.activation = 100; 62 | m.initialDescriptor.activation = 100; 63 | m.targetDescType.activation = 100; 64 | m.targetDescriptor.activation = 100; 65 | }); 66 | 67 | const urgency = Namespace.Codelets.CodeletUtils.getUrgencyBin(strength); 68 | const newCodelet = coderack.factory.create('correspondence-builder', urgency, [corresp]); 69 | coderack.post(newCodelet); 70 | } 71 | } 72 | 73 | }; 74 | 75 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/DescriptionBuilder.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 Description. 10 | * 11 | */ 12 | Namespace.Codelets.DescriptionBuilder = 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('description-builder', ctx, urgency, birthdate); 25 | this.description = args[0]; 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const descr = this.description; 35 | 36 | // Maybe fizzle 37 | if (!this.ctx.workspace.objects.includes(descr.object)) { return; } 38 | 39 | // Build or activate the description 40 | if (descr.object.hasDescriptor(descr.descriptor)) { 41 | descr.activate(); 42 | } 43 | else { 44 | descr.build(); 45 | } 46 | } 47 | }; 48 | 49 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/DescriptionStrengthTester.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 tests the strength of a Description, and if it's 9 | * strong enough, may post a DescriptionBuilder codelet. 10 | * 11 | */ 12 | Namespace.Codelets.DescriptionStrengthTester = 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('description-strength-tester', ctx, urgency, birthdate); 25 | this.description = args[0]; 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const ctx = this.ctx; 35 | const coderack = ctx.coderack; 36 | const description = this.description; 37 | 38 | // Maybe fizzle, if the strength is too low 39 | description.descriptor.activation = 100; 40 | description.updateStrength(); 41 | const strength = description.totalStrength; 42 | const prob = ctx.temperature.getAdjustedProb(strength/100); 43 | if (!ctx.randGen.coinFlip(prob)) { return; } 44 | 45 | // Post a DescriptionBuilder codelet 46 | const urgency = Namespace.Codelets.CodeletUtils.getUrgencyBin(strength); 47 | const newCodelet = coderack.factory.create('description-builder', urgency, [description]); 48 | coderack.post(newCodelet); 49 | } 50 | 51 | }; 52 | 53 | })( 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/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/Codelets/GroupStrengthTester.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 tests the strength of a Group, and if it's strong enough, 10 | * posts a GroupBuilder codelet. 11 | * 12 | */ 13 | Namespace.Codelets.GroupStrengthTester = 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('group-strength-tester', ctx, urgency, birthdate); 26 | this.group = args[0]; 27 | } 28 | 29 | 30 | /** 31 | * Runs the codelet. 32 | */ 33 | run() 34 | { 35 | const ctx = this.ctx; 36 | const coderack = ctx.coderack; 37 | const group = this.group; 38 | 39 | // Provide UI feedback 40 | if (ctx.ui && !ctx.batchMode) { 41 | ctx.ui.workspaceUi.getStringGraphic(group.string).groupsGraphic.flashProposed(group); 42 | } 43 | 44 | // Maybe fizzle, if the strength is too low 45 | group.updateStrength(); 46 | const strength = group.totalStrength; 47 | const prob = ctx.temperature.getAdjustedProb(strength/100); 48 | if (!ctx.randGen.coinFlip(prob)) { return; } 49 | 50 | // Post a GroupBuilder codelet 51 | group.groupCategory.getRelatedNode(ctx.slipnet.bondCategory).activation = 100; 52 | if (group.directionCategory) { group.directionCategory.activation = 100; } 53 | const urgency = Namespace.Codelets.CodeletUtils.getUrgencyBin(strength); 54 | const newCodelet = ctx.coderack.factory.create('group-builder', urgency, [group]); 55 | coderack.post(newCodelet); 56 | } 57 | 58 | 59 | 60 | 61 | }; 62 | 63 | })( 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/Codelets/ReplacementFinder.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 a letter at random from the initial string, 10 | * and marks it as changed if the letter at the same position in the 11 | * modified string is different. 12 | * 13 | */ 14 | Namespace.Codelets.ReplacementFinder = 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. (Empty for this codelet.) 22 | * @param {Number} birthdate - The birthdate of the codelet. 23 | */ 24 | constructor(ctx, urgency, args, birthdate) 25 | { 26 | super('replacement-finder', ctx, urgency, birthdate); 27 | } 28 | 29 | 30 | /** 31 | * Runs the codelet. 32 | * 33 | */ 34 | run() 35 | { 36 | const ctx = this.ctx; 37 | const sn = ctx.slipnet; 38 | const wksp = ctx.workspace; 39 | 40 | // Choose a random letter from the initial string 41 | const letters = wksp.initialWString.objects.filter(o => o instanceof Namespace.Letter); 42 | if ( letters.length < 1) { return; } 43 | 44 | const letterOfInitialString = ctx.randGen.choice(letters); 45 | if (letterOfInitialString.replacement) { return; } 46 | 47 | const position = letterOfInitialString.leftIndex; 48 | const modStringLength = wksp.modifiedWString.letters.length; 49 | const letterOfModifiedString = (position > modStringLength) ? null : wksp.modifiedWString.letters[position - 1]; 50 | if (letterOfModifiedString == null) { return; } 51 | 52 | const initialAscii = wksp.initialWString.jstring[position - 1].codePointAt(0); 53 | const modifiedAscii = wksp.modifiedWString.jstring[position - 1].codePointAt(0); 54 | const diff = initialAscii - modifiedAscii; 55 | const relation = (diff == -1) ? sn.successor : (diff === 0) ? sn.sameness : (diff == 1) ? sn.predecessor : null; 56 | 57 | const repl = new Namespace.Replacement(letterOfInitialString, letterOfModifiedString, relation); 58 | letterOfInitialString.replacement = repl; 59 | if (relation != sn.sameness) { 60 | letterOfInitialString.changed = true; 61 | wksp.changedObject = letterOfInitialString; 62 | } 63 | } 64 | }; 65 | 66 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/RuleBuilder.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 Rule, fighting with 9 | * competitors if necessary. 10 | * 11 | */ 12 | Namespace.Codelets.RuleBuilder = 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('rule-builder', ctx, urgency, birthdate); 25 | this.rule = 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 rule = this.rule; 37 | const Utils = Namespace.Codelets.CodeletUtils; 38 | 39 | // If this rule already exists, then fizzle. 40 | if ( rule.sameAs(wksp.rule) ) { 41 | rule.activate(); 42 | return; 43 | } 44 | 45 | // If the rule is too weak, then fizzle. 46 | rule.updateStrength(); 47 | if (rule.totalStrength === 0) { return; } 48 | 49 | // If a different rule already exists, then fight. 50 | if ( !wksp.rule || Utils.structureVsStructure(rule, 1.0, wksp.rule, 1.0)) { 51 | rule.build(); 52 | } 53 | } 54 | 55 | 56 | }; 57 | 58 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /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/Codelets/RuleStrengthTester.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 calculates a proposed Rule's strength, and decides 9 | * whether or not to post a RuleBuilder codelet. 10 | */ 11 | Namespace.Codelets.RuleStrengthTester = 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('rule-strength-tester', ctx, urgency, birthdate); 24 | this.rule = args[0]; 25 | } 26 | 27 | 28 | /** 29 | * Runs the codelet. 30 | */ 31 | run() 32 | { 33 | const ctx = this.ctx; 34 | const coderack = ctx.coderack; 35 | const rule = this.rule; 36 | 37 | // Maybe fizzle, if the strength is too low 38 | rule.updateStrength(); 39 | const strength = rule.totalStrength; 40 | const prob = ctx.temperature.getAdjustedProb(strength/100); 41 | if (!ctx.randGen.coinFlip(prob)) { return; } 42 | 43 | // Post a RuleBuilder codelet 44 | const urgency = Namespace.Codelets.CodeletUtils.getUrgencyBin(strength); 45 | const newCodelet = ctx.coderack.factory.create('rule-builder', urgency, [rule]); 46 | coderack.post(newCodelet); 47 | } 48 | 49 | 50 | 51 | 52 | }; 53 | 54 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/RuleTranslator.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 attempts to translate a Rule by applying the 9 | * slippages that have been built in the workspace. 10 | * 11 | */ 12 | Namespace.Codelets.RuleTranslator = 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('rule-translator', 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 | 36 | // If we don't have a rule, then fizzle. 37 | if (!wksp.rule) { return; } 38 | 39 | let bondDensity = 1.0; 40 | const totalLength = wksp.initialWString.length + wksp.targetWString.length; 41 | if (totalLength > 2) { 42 | const numBonds = wksp.initialWString.bonds.length + wksp.targetWString.bonds.length; 43 | bondDensity = Math.min(1.0, numBonds/(totalLength - 2)); 44 | } 45 | const weights = 46 | bondDensity > 0.8 ? [5, 150, 5, 2, 1, 1, 1, 1, 1, 1] : 47 | bondDensity > 0.6 ? [2, 5, 150, 5, 2, 1, 1, 1, 1, 1] : 48 | bondDensity > 0.4 ? [1, 2, 5, 150, 5, 2, 1, 1, 1, 1] : 49 | bondDensity > 0.2 ? [1, 1, 2, 5, 150, 5, 2, 1, 1, 1] : 50 | [1, 1, 1, 2, 5, 150, 5, 2, 1, 1]; 51 | 52 | const oneToTen = Array.from({length: 10}, (_, i) => i + 1); 53 | const cutoff = 10.0 * ctx.randGen.weightedChoice(oneToTen, weights); 54 | 55 | if (cutoff >= ctx.temperature.actualValue) { 56 | const result = wksp.rule.applyRuleToTarget(); 57 | if (result) { 58 | wksp.finalAnswer = result; 59 | } else { 60 | ctx.temperature.clampUntil(ctx.coderack.numCodeletsRun + 100); 61 | } 62 | } 63 | } 64 | 65 | }; 66 | 67 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/TopDownBondScout_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 Bonds between 9 | * neighboring object pairs in the initial or target string. 10 | * 11 | */ 12 | Namespace.Codelets.TopDownBondScout_Category = 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-bond-scout--category', ctx, urgency, birthdate); 25 | this.category = args[0]; 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const ctx = this.ctx; 35 | const sn = ctx.slipnet; 36 | const CodeletUtils = Namespace.Codelets.CodeletUtils; 37 | const cat = this.category; 38 | 39 | const bondSource = CodeletUtils.getScoutSource(ctx, 'bondCategory', cat); 40 | 41 | const bondDest = CodeletUtils.chooseNeighbor(ctx, bondSource); 42 | if (!bondDest) { return; } 43 | 44 | // Provide UI feedback. 45 | if (ctx.ui && !ctx.batchMode) { 46 | const dummyBond = new Namespace.Bond(bondSource, bondDest, sn.sameness, sn.letterCategory, sn.letters[0], sn.letters[0]); 47 | ctx.ui.workspaceUi.getStringGraphic(dummyBond.string). bondsGraphic.flashGrope(dummyBond); 48 | } 49 | 50 | const bondFacet = CodeletUtils.chooseBondFacet(ctx, bondSource, bondDest); 51 | if (!bondFacet) { return; } 52 | 53 | const sourceDescriptor = bondSource.getDescriptor(bondFacet); 54 | const destDescriptor = bondDest.getDescriptor(bondFacet); 55 | 56 | let forwardBond = sourceDescriptor.getBondCategory(destDescriptor); 57 | let backwardBond = null; 58 | if (forwardBond == sn.identity) { 59 | forwardBond = sn.sameness; 60 | backwardBond = sn.sameness; 61 | } else { 62 | backwardBond = destDescriptor.getBondCategory(sourceDescriptor); 63 | } 64 | 65 | if (cat == forwardBond) { 66 | ctx.coderack.proposeBond(bondSource, bondDest, cat, bondFacet, sourceDescriptor, destDescriptor); 67 | } 68 | else if (cat == backwardBond) { 69 | ctx.coderack.proposeBond(bondDest, bondSource, cat, bondFacet, destDescriptor, sourceDescriptor); 70 | } 71 | } 72 | 73 | }; 74 | 75 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/Codelets/TopDownBondScout_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 Bonds between 9 | * neighboring object pairs in the initial or target string. 10 | * 11 | */ 12 | Namespace.Codelets.TopDownBondScout_Direction = 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-bond-scout--direction', ctx, urgency, birthdate); 25 | this.bondDirection = args[0]; // left or right 26 | } 27 | 28 | 29 | /** 30 | * Runs the codelet. 31 | */ 32 | run() 33 | { 34 | const ctx = this.ctx; 35 | const CodeletUtils = Namespace.Codelets.CodeletUtils; 36 | 37 | const bondSource = CodeletUtils.getScoutSource(ctx, 'bondDirection', this.bondDirection); 38 | 39 | const bondDest = this._chooseDirectedNeighbor(bondSource); 40 | if (!bondDest) { return; } 41 | 42 | // Provide UI feedback. 43 | if (ctx.ui && !ctx.batchMode) { 44 | const sn = ctx.slipnet; 45 | const dummyBond = new Namespace.Bond(bondSource, bondDest, sn.sameness, sn.letterCategory, sn.letters[0], sn.letters[0]); 46 | ctx.ui.workspaceUi.getStringGraphic(dummyBond.string). bondsGraphic.flashGrope(dummyBond); 47 | } 48 | 49 | const bondFacet = CodeletUtils.chooseBondFacet(ctx, bondSource, bondDest); 50 | if (!bondFacet) { return; } 51 | 52 | const sourceDescriptor = bondSource.getDescriptor(bondFacet); 53 | const destDescriptor = bondDest.getDescriptor(bondFacet); 54 | 55 | let category = sourceDescriptor.getBondCategory(destDescriptor); 56 | if (!category) { return; } 57 | 58 | if (category == ctx.slipnet.identity) { category = ctx.slipnet.sameness; } 59 | 60 | // Propose the bond 61 | ctx.coderack.proposeBond(bondSource, bondDest, category, bondFacet, sourceDescriptor, destDescriptor); 62 | } 63 | 64 | 65 | 66 | /** 67 | * Chooses a neighbor of the given object in the given direction. 68 | * @private 69 | */ 70 | _chooseDirectedNeighbor(source) 71 | { 72 | const ctx = this.ctx; 73 | let objects = []; 74 | 75 | if (this.bondDirection == ctx.slipnet.left) { 76 | objects = ctx.workspace.objects.filter(o => (o.string == source.string) && (source.leftIndex == o.rightIndex + 1)); 77 | } else { 78 | objects = ctx.workspace.objects.filter(o => (o.string == source.string) && (source.rightIndex == o.leftIndex - 1)); 79 | } 80 | 81 | const weights = objects.map( o => ctx.temperature.getAdjustedValue(o.intraStringSalience) ); 82 | return ctx.randGen.weightedChoice(objects, weights); 83 | } 84 | 85 | }; 86 | 87 | })( 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/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/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/ConsoleReporter.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 used to report information to the console. 9 | * 10 | */ 11 | Namespace.ConsoleReporter = class { 12 | 13 | /** 14 | * @constructor 15 | * 16 | */ 17 | constructor() 18 | { } 19 | 20 | 21 | /** 22 | * Reports an informational message. 23 | */ 24 | info(msg) 25 | { 26 | console.info(msg); 27 | } 28 | 29 | 30 | /** 31 | * Reports a warning message. 32 | */ 33 | warn(msg) 34 | { 35 | console.warn(msg); 36 | } 37 | 38 | 39 | /** 40 | * Reports an error message. 41 | */ 42 | error(msg) 43 | { 44 | console.error(msg); 45 | } 46 | 47 | }; 48 | 49 | 50 | })( 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/Letter.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 Letter class represents a single alphabetic character. 9 | * It inherits from WorkspaceObject. 10 | * 11 | */ 12 | Namespace.Letter = class extends Namespace.WorkspaceObject { 13 | 14 | /** 15 | * @constructor 16 | * 17 | * @param {WorkspaceString} str - The string that the letter is in. 18 | * @param {Number} position - The (1-based) position of the letter within the string. 19 | */ 20 | constructor(str, position) 21 | { 22 | super(str); 23 | this.char = str.jstring.charAt(position-1); 24 | this.position = position; 25 | this.leftIndex = position; 26 | this.rightIndex = position; 27 | this.leftmost = (position == 1); 28 | this.rightmost = (position == str.length); 29 | 30 | // Create and cache my descriptions 31 | this._addDescriptions(); 32 | } 33 | 34 | 35 | /** 36 | * Creates and caches the descriptions of the Letter. 37 | * @private 38 | */ 39 | _addDescriptions() 40 | { 41 | const sn = this.wksp.ctx.slipnet; 42 | this.descriptions.push(new Namespace.Description(this, sn.objectCategory, sn.letter)); 43 | 44 | const letterNode = sn.letters[this.string.jstring.toUpperCase().charCodeAt(this.position-1) - 'A'.charCodeAt(0)]; 45 | this.descriptions.push(new Namespace.Description(this, sn.letterCategory, letterNode)); 46 | 47 | if (this.string.length == 1) { 48 | this.descriptions.push( new Namespace.Description(this, sn.stringPositionCategory, sn.single) ); 49 | } 50 | if (this.leftmost) { 51 | this.descriptions.push( new Namespace.Description(this, sn.stringPositionCategory, sn.leftmost) ); 52 | } 53 | if (this.rightmost) { 54 | this.descriptions.push( new Namespace.Description(this, sn.stringPositionCategory, sn.rightmost) ); 55 | } 56 | if (2*this.position == this.string.length + 1) { 57 | this.descriptions.push( new Namespace.Description(this, sn.stringPositionCategory, sn.middle) ); 58 | } 59 | } 60 | 61 | 62 | /** 63 | * Returns a string describing the object. 64 | * 65 | */ 66 | synopsis(type) 67 | { 68 | return !type ? this.char : ''; 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/RandGen.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 methods for generating random values 9 | * and making random selections. 10 | * 11 | */ 12 | Namespace.RandGen = class { 13 | 14 | /** 15 | * @constructor 16 | * @param {Number|String} [seed=Date.now] - A seed for initializing 17 | * the randomness generator. 18 | */ 19 | constructor(seed) 20 | { 21 | // Initialize 22 | const strSeed = ((typeof(seed) !== "undefined") && (seed !== null)) ? 23 | seed.toString() : Date.now().toString(); 24 | 25 | const cyrSeed = Namespace.RandGen._get_seed_cyrb128(strSeed); 26 | this.rng = Namespace.RandGen._sfc32(...cyrSeed); 27 | } 28 | 29 | 30 | /** 31 | * Returns a random number in the range [0,1), sampled from 32 | * a uniform distribution. 33 | * 34 | * @returns A number in the range [0,1). 35 | */ 36 | rand() 37 | { 38 | return this.rng(); 39 | } 40 | 41 | 42 | /** 43 | * Returns true or false, with probabilities p and 1-p, respectively. 44 | * 45 | * @param {Number} p - The probability of a 'true' result. 46 | * @returns true or false. 47 | */ 48 | coinFlip(p = 0.5) 49 | { 50 | return this.rng() < p; 51 | } 52 | 53 | 54 | /** 55 | * Returns a random element from a given sequence. 56 | * 57 | * @param {Indexable} seq - The sequence to select from. 58 | * @returns A random element from seq. 59 | */ 60 | choice(seq) 61 | { 62 | const idx = Math.floor(this.rng()*seq.length); 63 | return seq[idx]; 64 | } 65 | 66 | 67 | /** 68 | * Returns a random element from the given sequence, 69 | * with weighted probabilities. 70 | * 71 | * @param {Indexable} seq - The sequence to select from. 72 | * @param {Array} 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/Replacement.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 Replacement class encapsulates a relation (sameness, predecessor, or 9 | * successor) between a Letter in the initial string and a Letter in the 10 | * modified string. 11 | * 12 | */ 13 | Namespace.Replacement = class { 14 | 15 | /** 16 | * @constructor 17 | * 18 | * @param {Letter} objFromInitial - A letter from the initial string. 19 | * @param {Letter} objFromModified - A letter from the modified string. 20 | * @param {SlipNode} relation - The relation between the two letters. 21 | * 22 | */ 23 | constructor(objFromInitial, objFromModified, relation) 24 | { 25 | // WorkspaceStructure members 26 | this.wksp = objFromInitial.wksp; 27 | this.string = objFromInitial.string; 28 | this.totalStrength = 0; 29 | 30 | this.objFromInitial = objFromInitial; 31 | this.objFromModified = objFromModified; 32 | this.relation = relation; 33 | } 34 | 35 | 36 | /** 37 | * Returns a string describing the object. 38 | * 39 | */ 40 | synopsis(type) 41 | { 42 | let s = this.objFromInitial.synopsis() + ' -> ' + 43 | this.objFromModified.synopsis() + ' (' + 44 | (this.relation ? this.relation.name : 'null') + ')'; 45 | 46 | return !type ? s : ''; 47 | } 48 | 49 | }; 50 | 51 | 52 | })( window.CopycatJS = window.CopycatJS || {} ); -------------------------------------------------------------------------------- /src/SlipLink.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 SlipLink class represents a relation between two concepts. 9 | * 10 | */ 11 | Namespace.SlipLink = class { 12 | 13 | /** 14 | * @constructor 15 | * 16 | * @param {String} type - The link type ('category', 'property', 'instance', 'lateralSlip', or 'lateralNonSlip'). 17 | * @param {SlipNode} source - The source node. 18 | * @param {SlipNode} destination - The destination node. 19 | * @param {SlipNode} [label=null] - A SlipNode that labels the link. 20 | * @param {Number} [length=0] - The "conceptual distance" between the source and destination nodes. Slippage 21 | * occurs more easily when this distance is smaller. 22 | */ 23 | constructor(type, source, destination, label=null, length=0) 24 | { 25 | this.type = type; 26 | this.source = source; 27 | this.destination = destination; 28 | this.label = label; 29 | this.fixedLength = length; 30 | 31 | // Add this link to the source and destination nodes. 32 | source.outgoingLinks.push(this); 33 | destination.incomingLinks.push(this); 34 | 35 | Object.freeze(this); 36 | } 37 | 38 | 39 | /** 40 | * Returns a string describing the object. 41 | * 42 | */ 43 | synopsis(type) 44 | { 45 | const s = `${this.source.synopsis(1)} to ` + 46 | `${this.destination.synopsis(1)} (length=` + 47 | `${this.fixedLength.toFixed(0)}` + 48 | (this.label ? `, label=${this.label.name}` : '') + ')'; 49 | 50 | return !type ? s : ''; 51 | } 52 | 53 | 54 | /** 55 | * Returns a measure of the shortness of this link, or 56 | * of its label if it has one. 57 | * 58 | */ 59 | degreeOfAssociation() 60 | { 61 | if ((this.fixedLength > 0) || !this.label) { 62 | return 100 - this.fixedLength; 63 | } 64 | else { 65 | return this.label.degreeOfAssociation(); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Returns a measure of the shortness of this link, or 72 | * of its label's intrinsic shortness if it has a label. 73 | * 74 | */ 75 | intrinsicDegreeOfAssociation() 76 | { 77 | if (this.fixedLength !== 0) { 78 | return 100 - this.fixedLength; 79 | } 80 | else if (this.label) { 81 | return 100 - this.label.intrinsicLinkLength; 82 | } 83 | else { 84 | return 0; 85 | } 86 | } 87 | 88 | 89 | }; 90 | 91 | })( 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/UI/ArrowGraphic.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 string-to-corresponding-string arrows. 9 | * 10 | */ 11 | Namespace.ArrowGraphic = class 12 | { 13 | /** 14 | * @constructor 15 | * 16 | * @param {WorkspaceUi} workspaceUi - The parent Ui. 17 | */ 18 | constructor(workspaceUi) 19 | { 20 | this.wkspUi = workspaceUi; 21 | this.drawParams = {}; 22 | } 23 | 24 | 25 | /** 26 | * Draws the arrows. 27 | * 28 | */ 29 | redraw(ctx) 30 | { 31 | // Update our drawing parameters if necessary 32 | if (Namespace.UiUtils.NeedToRescale(this.drawParams, ctx)) { this._updateDrawParams(ctx);} 33 | 34 | const dp = this.drawParams; 35 | ctx.strokeStyle = this.wkspUi.letterColor; 36 | ctx.lineWidth = 1; 37 | ctx.setLineDash([]); 38 | 39 | for (let line of dp.lines) { 40 | Namespace.UiUtils.DrawLine(ctx, line.xa, line.ya, line.xb, line.yb); 41 | } 42 | } 43 | 44 | 45 | /** 46 | * Recalculates the drawing parameters for this object. 47 | * 48 | * @private 49 | */ 50 | _updateDrawParams(ctx) 51 | { 52 | const [w, h] = [ctx.canvas.width, ctx.canvas.height]; 53 | const dp = this.drawParams; 54 | dp.canvasWidth = w; dp.canvasHeight = h; 55 | 56 | let cx = w/2; 57 | let cy = h/3 - h/80; 58 | let sz = w/25; 59 | dp.lines = []; 60 | 61 | dp.lines.push({xa: cx-0.5*sz, ya: cy-0.08*sz, xb: cx+0.34*sz, yb: cy-0.08*sz}); 62 | dp.lines.push({xa: cx-0.5*sz, ya: cy+0.08*sz, xb: cx+0.34*sz, yb: cy+0.08*sz}); 63 | dp.lines.push({xa: cx+0.1*sz, ya: cy-0.2*sz, xb: cx+0.5*sz, yb: cy}); 64 | dp.lines.push({xa: cx+0.1*sz, ya: cy+0.2*sz, xb: cx+0.5*sz, yb: cy}); 65 | 66 | cy = 2*h/3 - h/80; 67 | dp.lines.push({xa: cx-0.5*sz, ya: cy-0.08*sz, xb: cx+0.34*sz, yb: cy-0.08*sz}); 68 | dp.lines.push({xa: cx-0.5*sz, ya: cy+0.08*sz, xb: cx+0.34*sz, yb: cy+0.08*sz}); 69 | dp.lines.push({xa: cx+0.1*sz, ya: cy-0.2*sz, xb: cx+0.5*sz, yb: cy}); 70 | dp.lines.push({xa: cx+0.1*sz, ya: cy+0.2*sz, xb: cx+0.5*sz, yb: cy}); 71 | } 72 | }; 73 | 74 | 75 | 76 | 77 | 78 | })( 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/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 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 | '
  • abcabd,   kji → ?
  • ' + 44 | '
  • abcabd,   iijjkk → ?
  • ' + 45 | '
  • abcabd,   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 | } 72 | 73 | }; 74 | 75 | })( window.CopycatJS = window.CopycatJS || {} ); 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /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/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/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/RuleGraphic.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 final Rule that copycat has found. 9 | * 10 | */ 11 | Namespace.RuleGraphic = 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.wksp = workspaceUi.workspace; 23 | this.drawParams = {}; 24 | } 25 | 26 | 27 | /** 28 | * Draws the Rule text. 29 | * 30 | */ 31 | redraw(ctx) 32 | { 33 | if (!this.wksp.finalAnswer) { return; } 34 | 35 | const ruleText = this.wksp.rule ? this.wksp.rule.synopsis(0) : ""; 36 | if (ruleText) { 37 | this._updateDrawParams(ctx, ruleText); 38 | 39 | const dp = this.drawParams; 40 | ctx.strokeStyle = this.wkspUi.ruleColor; 41 | ctx.fillStyle = this.wkspUi.ruleColor; 42 | ctx.textAlign = 'center'; 43 | ctx.setLineDash([]); 44 | ctx.font = dp.font; 45 | dp.textLines.forEach(line => 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/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/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/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 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/btn_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/src/UI/btn_pause.png -------------------------------------------------------------------------------- /src/UI/btn_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/src/UI/btn_play.png -------------------------------------------------------------------------------- /src/UI/btn_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/src/UI/btn_reset.png -------------------------------------------------------------------------------- /src/UI/btn_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/src/UI/btn_step.png -------------------------------------------------------------------------------- /src/UI/cc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paul-G2/copycat-js/536ddbf5f776ac25aeb8081269a1808364b3c26a/src/UI/cc_logo.png -------------------------------------------------------------------------------- /src/UI/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #000000; 5 | } 6 | div#frame_area { 7 | position: relative; 8 | width: 100vw; 9 | height: 100vh; 10 | height: 100dvh; 11 | margin: 0; 12 | padding: 0; 13 | overflow: hidden; 14 | background: #7092BE; 15 | } 16 | div#app_area { 17 | margin: 0; 18 | padding: 0; 19 | position: absolute; 20 | top: 0px; 21 | right: 0px; 22 | bottom: 0px; 23 | left: 0px; 24 | background: #e0d20b; 25 | } 26 | div#topbar_area { 27 | margin: 0; 28 | padding: 0; 29 | position: absolute; 30 | top: 0px; 31 | height: 7%; 32 | left: 0px; 33 | right: 0px; 34 | background: #bfcbdf; 35 | } 36 | div#widgets_area { 37 | margin: 0; 38 | padding: 0; 39 | position: absolute; 40 | top: 7%; 41 | height: 93%; 42 | left: 0px; 43 | right: 0px; 44 | background: #a870be; 45 | } 46 | div#input_area { 47 | margin: 0; 48 | padding: 0; 49 | position: absolute; 50 | top: 0px; 51 | height: 15%; 52 | left: 0px; 53 | width: 60%; 54 | background: #b3ddcc; 55 | } 56 | div#workspace_header_area { 57 | margin: 0; 58 | padding: 0; 59 | position: absolute; 60 | top: 15%; 61 | height: 12%; 62 | left: 0px; 63 | width: 60%; 64 | background: #eeeeee; 65 | } 66 | div#workspace_area { 67 | margin: 0; 68 | padding: 0; 69 | position: absolute; 70 | top: 27%; 71 | height: 73%; 72 | left: 0px; 73 | width: 60%; 74 | background: #ffffff; 75 | } 76 | div#batchmode_area { 77 | margin: 0; 78 | padding: 0; 79 | position: absolute; 80 | top: 15%; 81 | height: 85%; 82 | left: 0px; 83 | width: 100%; 84 | background: #ffffff; 85 | display: none; 86 | } 87 | div#slipnet_area { 88 | margin: 0; 89 | padding: 0; 90 | position: absolute; 91 | top: 0px; 92 | bottom: 0px; 93 | left: 60%; 94 | width: 25%; 95 | background: #fffbcc; 96 | } 97 | div#coderack_area { 98 | margin: 0; 99 | padding: 0; 100 | position: absolute; 101 | top: 0px; 102 | bottom: 0px; 103 | left: 85%; 104 | width: 15%; 105 | background: #ffe5e0; 106 | } 107 | .noselect { 108 | -webkit-touch-callout: none; 109 | -webkit-user-select: none; 110 | -khtml-user-select: none; 111 | -moz-user-select: none; 112 | -ms-user-select: none; 113 | user-select: none; 114 | } 115 | p + ul { 116 | margin-top: -16px; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /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/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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copycat 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | --------------------------------------------------------------------------------