From dfbc4f313f1e7c88eae4f1f2278855f18d97ec1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:49:10 +0000 Subject: [PATCH] Deploy to GitHub pages --- asset-manifest.json | 13 ++++++++ favicon.ico | Bin 0 -> 3870 bytes index.html | 1 + logo192.png | Bin 0 -> 5347 bytes logo512.png | Bin 0 -> 9664 bytes manifest.json | 25 +++++++++++++++ robots.txt | 3 ++ static/css/main.101ab0f3.css | 2 ++ static/css/main.101ab0f3.css.map | 1 + static/js/main.202c3475.js | 3 ++ static/js/main.202c3475.js.LICENSE.txt | 41 +++++++++++++++++++++++++ static/js/main.202c3475.js.map | 1 + 12 files changed, 90 insertions(+) create mode 100644 asset-manifest.json create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 logo192.png create mode 100644 logo512.png create mode 100644 manifest.json create mode 100644 robots.txt create mode 100644 static/css/main.101ab0f3.css create mode 100644 static/css/main.101ab0f3.css.map create mode 100644 static/js/main.202c3475.js create mode 100644 static/js/main.202c3475.js.LICENSE.txt create mode 100644 static/js/main.202c3475.js.map diff --git a/asset-manifest.json b/asset-manifest.json new file mode 100644 index 00000000..1b46c652 --- /dev/null +++ b/asset-manifest.json @@ -0,0 +1,13 @@ +{ + "files": { + "main.css": "/solver/static/css/main.101ab0f3.css", + "main.js": "/solver/static/js/main.202c3475.js", + "index.html": "/solver/index.html", + "main.101ab0f3.css.map": "/solver/static/css/main.101ab0f3.css.map", + "main.202c3475.js.map": "/solver/static/js/main.202c3475.js.map" + }, + "entrypoints": [ + "static/css/main.101ab0f3.css", + "static/js/main.202c3475.js" + ] +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 00000000..c7d52bc1 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +Sudoku solver
\ No newline at end of file diff --git a/logo192.png b/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..3823d9f9 --- /dev/null +++ b/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Sudoku solver", + "name": "Yet another sudoku solver", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#49515f", + "background_color": "#3e4451" +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/static/css/main.101ab0f3.css b/static/css/main.101ab0f3.css new file mode 100644 index 00000000..4acc2bb8 --- /dev/null +++ b/static/css/main.101ab0f3.css @@ -0,0 +1,2 @@ +*,:after,:before{background-repeat:no-repeat;box-sizing:border-box}:after,:before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;cursor:default;line-height:1.5;overflow-wrap:break-word;tab-size:4}:where(body){margin:0}:where(h1){font-size:2em;margin:.67em 0}:where(dl,ol,ul) :where(dl,ol,ul){margin:0}:where(hr){color:inherit;height:0}:where(nav) :where(ol,ul){list-style-type:none;padding:0}:where(nav li):before{content:"\200B";float:left}:where(pre){font-family:monospace,monospace;font-size:1em;overflow:auto}:where(abbr[title]){text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,samp){font-family:monospace,monospace;font-size:1em}:where(small){font-size:80%}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}:where(iframe){border-style:none}:where(svg:not([fill])){fill:currentColor}:where(table){border-collapse:collapse;border-color:inherit;text-indent:0}:where(button,input,select){margin:0}:where(button,[type=button i],[type=reset i],[type=submit i]){-webkit-appearance:button}:where(fieldset){border:1px solid #a0a0a0}:where(progress){vertical-align:initial}:where(textarea){margin:0;resize:vertical}:where([type=search i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(dialog){background-color:#fff;border:solid;color:#000;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(details>summary:first-of-type){display:list-item}:where([aria-busy=true i]){cursor:progress}:where([aria-controls]){cursor:pointer}:where([aria-disabled=true i],[disabled]){cursor:not-allowed}:where([aria-hidden=false i][hidden]){display:initial}:where([aria-hidden=false i][hidden]:not(:focus)){clip:rect(0,0,0,0);position:absolute}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}.monospace,code{font-family:var(--monospace)}:root{--monospace:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace;color-scheme:dark;font-size:calc(.5em + 1vmin)}@media print{:root{--text-color:#000;--background-color:#fff;--link-color:#00e;--link-active-color:red;--link-visited-color:#551a8b}}:root{--relative-pixel:0.04ch;--regular-border:calc(var(--relative-pixel)*2);--larger-border:calc(var(--regular-border)*8);--large-border:calc(var(--larger-border)/3);--sudokucell-length:4rem;--cell-fontsize:2.5rem;--candidate-fontsize:0.9rem;--background-color:#282c34;--link-color:#61dafb;--link-visited-color:#d7a1e8;--link-active-color:tomato;--text-color:#f5f5f5;--border-color:#fff;--maroon:maroon;--red:#ec3e31;--orange:orange;--blue-gray:#8cadf3;--lighter-green:#8ea;--light-green:#0c0;--green:green;--purple:#639}.App{grid-gap:1rem;--app-main-height:calc(var(--sudokucell-length)*9 + var(--regular-border)*7*2 + var(--regular-border)*2*1 + var(--regular-border)*16/3);align-content:space-between;background-color:var(--background-color);color:var(--text-color);display:grid;gap:1rem;margin:0;min-height:100vh;min-width:100vw;position:relative;width:-moz-fit-content;width:fit-content}@media (min-width:900px){.App{gap:0 0;grid-template-areas:"Header Header Header Header Header" ". Main . Aside ." ". . . . .";grid-template-columns:250fr 1432fr 150fr 1314fr 250fr;grid-template-rows:auto var(--app-main-height) .3fr}.App-aside{--font-size:1rem}}@media (max-width:900px){.App{grid-template-areas:". Header ." ". Main ." ". Aside .";grid-template-columns:250fr 2745fr 250fr;grid-template-rows:auto var(--app-main-height) auto;justify-content:center}.App-aside{--font-size:calc(1rem + 1vh);font-size:var(--font-size);margin:1.5rem 0 25vh}}.App-header{font-size:calc(10px + 2vmin);grid-area:Header;height:-moz-fit-content;height:fit-content;padding:2vh 0}.App-header>h1.Title{display:inline;font-size:1em;margin:0;padding:0 2vw}.App-header>.Version{color:silver;font-size:.7em}.App.error{border:.5rem solid var(--red)}.App.error>.App-header:before{color:var(--red);content:"Error! "}.App-link{color:var(--link-color)}.App-link:visited{color:var(--link-visited-color)}.App-link:active{color:var(--link-active-color)}.App-aside{grid-area:Aside}.App-main{--sudoku-length:calc(var(--sudokucell-length)*9 + var(--regular-border)*7*2 + var(--regular-border)*2*1 + var(--large-border)*2);align-content:center;display:flex;grid-area:Main;justify-content:center}.App-main,.Sudoku{height:var(--sudoku-length)}.Sudoku{width:var(--sudoku-length);z-index:3}table.Sudoku{--cell-height:11.11111%;border:var(--larger-border) solid var(--border-color);border-collapse:collapse;border-spacing:0;cursor:pointer;font-family:var(--monospace);table-layout:fixed;text-align:center;vertical-align:middle}tr.Row{height:var(--cell-height)}tr.Row:nth-child(3n):not(:last-child)>td.Cell{border-bottom:var(--large-border) solid var(--border-color)}td.Cell{border:var(--regular-border) solid var(--border-color);border-spacing:0;font-size:var(--cell-fontsize);height:var(--cell-height);max-height:var(--cell-height);max-width:var(--cell-height);padding:0;text-align:center;vertical-align:middle;width:var(--cell-height)}td.Cell:nth-child(3n):not(:last-child){border-right:var(--large-border) solid var(--border-color)}[dir=rtl] td.Cell:nth-child(3n):not(:last-child),td.Cell:nth-child(3n):not(:last-child):dir(rtl){border-left:var(--large-border) solid var(--border-color);border-right:initial}div.Cell{background-color:initial;border:none;height:100%;padding:0;width:100%}div.Cell>*{height:var(--cell-height);overflow:hidden;text-overflow:clip}div.Cell>span.ugh.tables{align-items:center;display:inline-flex;height:100%}div.Cell[data-error=true]{background-color:var(--maroon)}div.Cell[data-active=true]{background-color:#555}div.Cell[data-active=false]{background:repeating-linear-gradient(-45deg,#d35e5e,#44476b,#333)}.Cell>.Loading{font-size:var(--candidate-fontsize);text-overflow:ellipsis}p.Candidates{display:grid;font-size:1rem;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);height:100%;margin:0;padding:2pt;width:100%}span.Candidate{color:inherit;font-size:var(--candidate-fontsize);line-height:normal;overflow:hidden;padding:0;text-align:center;text-overflow:clip}.Candidate.eliminated{background-color:#ff0}.Candidate.added{background-color:green}.Candidate.solved{color:#90ee90}.Candidate.blue,.Cell.blue{background-color:var(--blue-gray);color:#000}.Candidate.green,.Cell.green{background-color:#90ee90;color:#000}.Candidate.orange{background-color:orange;color:#000}.Cell.orange{background-color:#f0c575;color:#000}.Cell.salmon{background-color:salmon;color:#000}.Cell.orange.salmon{background-color:#f5a675;color:#000}.Candidate.blue.green,.Cell.blue.green{background-color:#8ecec2}.App-aside{border:inset;height:-moz-fit-content;height:fit-content}.StrategyList{margin-bottom:0;margin-left:1rem;margin-top:0;min-width:-moz-fit-content;min-width:fit-content}.StrategyItem.isCurrent{border-right:5px solid #b7c1c2}@supports selector(:dir(rtl)){.StrategyItem.isCurrent:dir(rtl){border-left:5px solid #b7c1c2;border-right:initial}}@supports not selector(:dir(rtl)){.StrategyItem.isCurrent[dir=rtl]{border-left:5px solid #b7c1c2;border-right:initial}}.StrategyItem:nth-child(odd){background-color:#1f2329}.StrategyItem:nth-child(2n){background-color:#31353f}.StrategyItem.disabled{color:#aaa}.StrategyItem.disabled:nth-child(odd){background-color:#282b31}.StrategyItem.disabled:nth-child(2n){background-color:#31353c}.StrategyList{--item-height:var(--font-size);max-width:none;padding-left:calc(2rem + var(--item-height));position:relative}.StrategyItem{--label-width:80%;--status-width:20%;--padding:1.5rem;line-height:calc(var(--item-height) + .1rem);max-width:calc(100% - var(--padding));min-width:calc(var(--label-width) + var(--status-width));padding-left:var(--padding)}.StrategyToggler{height:var(--item-height);left:-1rem;line-height:inherit;margin:0;position:absolute;text-align:center;width:var(--item-height)}@supports selector(:dir(rtl)){.StrategyList:dir(rtl){padding-left:0;padding-right:calc(2rem + var(--item-height))}.StrategyItem:dir(rtl){padding-left:0;padding-right:var(--padding)}.StrategyToggler:dir(rtl){left:auto;right:0}}@supports not selector(:dir(rtl)){[dir=rtl] .StrategyList{padding-left:0;padding-right:calc(2rem + var(--item-height))}[dir=rtl] .StrategyItem{padding-left:0;padding-right:var(--padding)}[dir=rtl] .StrategyToggler{left:auto;right:0}}.StrategyLabel{display:inline-grid;width:var(--label-width)}.StrategyTogglerLabel{color:#0000;height:var(--item-height);overflow:hidden;position:absolute;right:0;text-overflow:clip;-webkit-user-select:none;user-select:none;width:100%}a[href]+label>.StrategyTogglerLabel{width:20%}.StrategyResult{grid-area:status}.StrategyResult.success{color:var(--light-green)}.StrategyResult.fail{color:var(--orange)}.StrategyResult.error{color:var(--red)}.StrategyDetails{border:1px solid #a0a0a0;margin-inline-end:2px;margin-inline-start:2px;padding:.5em}.StrategyDetails>p{margin:0;white-space:pre-wrap;word-break:break-word}.Tabs{display:grid;grid-template-columns:repeat(2,1fr)}.Tab.selected{background-color:var(--background-color);border:none}.Tab.unselected{background-color:#505050;border-style:outset}.Tab.unselected:hover{background-color:#767676}.Tab.unselected:active{background-color:#373737;border-style:inset}.AlertNotice{word-wrap:break-word;background-color:#444;border-style:solid;border-width:var(--regular-border);bottom:2vh;display:flex;flex-direction:column;justify-content:space-between;max-width:calc(100% - 4rem);padding:0 1rem 1rem;position:fixed;right:2vw;width:-moz-fit-content;width:fit-content;word-break:break-all;z-index:4}.AlertNotice,.AlertNotice>p{height:-moz-fit-content;height:fit-content}.AlertNotice>p{font-size:1rem}.AlertNotice>button{font-size:2rem}.AlertNotice>*{position:sticky}.AlertNotice.info{border-color:var(--blue-gray)}.AlertNotice.warning{border-color:var(--orange)}.AlertNotice.error{border-color:var(--red)}.PromptWindow{background-color:#7777;display:flex;height:100vh;position:fixed;width:100vw;z-index:4}.PromptNotice{word-wrap:break-word;align-content:center;align-self:center;background-color:var(--background-color);display:inline-flex;flex-direction:column;height:-moz-fit-content;height:fit-content;justify-content:center;margin:0 auto;padding:1rem;position:relative;text-align:center;width:-moz-fit-content;width:fit-content;word-break:break-all;z-index:4}.PromptNotice>label{font-size:1.5rem}.PromptNotice>button{font-size:1.5rem;width:100%}.PromptNotice>label>p{margin-bottom:1rem;margin-top:0}.PromptNotice>label>textarea{width:100%}.github-corner{border:0;color:var(--text-color);position:absolute;right:0;top:0}.github-corner>svg{fill:#151513}.octo-arm{transform-origin:130px 106px}@keyframes octocat-wave{0%,to{transform:rotate(0deg)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (prefers-reduced-motion:no-preference){.github-corner:hover .octo-arm{animation:octocat-wave .56s ease-in-out}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave .56s ease-in-out}}}@media (prefers-reduced-motion:reduce){.github-corner:hover .octo-arm{animation:octocat-wave 7s ease-in-out}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 7s ease-in-out}}}.ErrorNotice{border:3px solid var(--red);font-family:var(--monospace);height:-moz-fit-content;height:fit-content;padding:1.5rem;white-space:pre;width:-moz-fit-content;width:fit-content} +/*# sourceMappingURL=main.101ab0f3.css.map*/ \ No newline at end of file diff --git a/static/css/main.101ab0f3.css.map b/static/css/main.101ab0f3.css.map new file mode 100644 index 00000000..7aeb981b --- /dev/null +++ b/static/css/main.101ab0f3.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.101ab0f3.css","mappings":"AAQA,iBAIE,2BAA4B,CAD5B,qBAEF,CAOA,eAEE,uBAAwB,CACxB,sBACF,CAWA,cAME,uCAAwC,CACxC,6BAA8B,CAN9B,cAAe,CACf,eAAgB,CAChB,wBAAyB,CAEzB,UAGF,CASA,aACE,QACF,CAOA,WACE,aAAc,CACd,cACF,CASA,kCACE,QACF,CAOA,WACE,aAAc,CACd,QACF,CAMA,0BACE,oBAAqB,CACrB,SACF,CAMA,sBACE,eAAgB,CAChB,UACF,CAQA,YACE,+BAAiC,CACjC,aAAc,CACd,aACF,CASA,oBACE,yBAA0B,CAC1B,wCAAiC,CAAjC,gCACF,CAMA,iBACE,kBACF,CAOA,sBACE,+BAAiC,CACjC,aACF,CAMA,cACE,aACF,CASA,0CACE,qBACF,CAMA,eACE,iBACF,CAMA,wBACE,iBACF,CAWA,cACE,wBAAyB,CACzB,oBAAqB,CACrB,aACF,CASA,4BACE,QACF,CAMA,8DACE,yBACF,CAMA,iBACE,wBACF,CAMA,iBACE,sBACF,CAOA,iBACE,QAAS,CACT,eACF,CAOA,wBACE,4BAA6B,CAC7B,mBACF,CAMA,wDAEE,WACF,CAMA,4BACE,aAAc,CACd,WACF,CAMA,4BACE,uBACF,CAOA,6BACE,yBAA0B,CAC1B,YACF,CASA,eACE,qBAAuB,CACvB,YAAa,CACb,UAAY,CACZ,uBAAwB,CACxB,kBAAmB,CACnB,MAAO,CACP,WAAY,CACZ,WAAY,CACZ,iBAAkB,CAClB,OAAQ,CACR,sBAAuB,CACvB,iBACF,CAEA,2BACE,YACF,CAMA,sCACE,iBACF,CASA,2BACE,eACF,CAMA,wBACE,cACF,CAOA,0CACE,kBACF,CAOA,sCACE,eACF,CAEA,kDACE,kBAAsB,CACtB,iBACF,CCxWA,KAKG,kCAAmC,CACnC,iCAAkC,CAJlC,mIAEa,CAHb,QAMH,CAEA,gBAEG,4BACH,CAEA,MACG,yEAA+E,CAM/E,iBAAkB,CAHlB,4BAIH,CAEA,aACG,MACG,iBAAmB,CACnB,uBAAyB,CACzB,iBAA4B,CAC5B,uBAAwB,CACxB,4BACH,CACH,CChCA,MACG,uBAAgC,CAGhC,8CAAiD,CACjD,6CAAgD,CAChD,2CAA8C,CAE9C,wBAAyB,CACzB,sBAAuB,CACvB,2BAA4B,CAG5B,0BAA2B,CAC3B,oBAAqB,CACrB,4BAA6B,CAC7B,0BAA2B,CAC3B,oBAAwB,CACxB,mBAAqB,CACrB,eAAgB,CAChB,aAAc,CACd,eAAgB,CAChB,mBAAoB,CACpB,oBAAqB,CACrB,kBAAmB,CACnB,aAAc,CACd,aACH,CAEA,KAaG,aAAS,CAKT,uIAOC,CAXD,2BAA4B,CAb5B,wCAAyC,CACzC,uBAAwB,CAUxB,YAAa,CACb,QAAS,CAHT,QAAS,CAHT,gBAAiB,CACjB,eAAgB,CAHhB,iBAAkB,CAIlB,sBAAkB,CAAlB,iBAiBH,CAGA,yBACG,KAGG,OAAQ,CACR,uFAGc,CANd,qDAAsD,CACtD,mDAMH,CAGA,WACG,gBACH,CACH,CAGA,yBACG,KAGG,uDAGc,CALd,wCAAyC,CACzC,mDAAoD,CAMpD,sBACH,CAOA,WACG,4BAA6B,CAC7B,0BAA2B,CAC3B,oBACH,CACH,CAwBA,YAIG,4BAA6B,CAH7B,gBAAiB,CAEjB,uBAAmB,CAAnB,kBAAmB,CADnB,aAGH,CAEA,qBACG,cAAe,CACf,aAAc,CAEd,QAAS,CADT,aAEH,CAEA,qBAEG,YAAa,CADb,cAEH,CAGA,WACG,6BACH,CAEA,8BAEG,gBAAiB,CADjB,iBAEH,CAGA,UACG,uBACH,CAEA,kBACG,+BACH,CAEA,iBACG,8BACH,CAEA,WAAa,eAAkB,CAC/B,UCrKG,gIAKC,CAgBD,oBAAqB,CAHrB,YAAa,CDmJJ,cAAe,CCjJxB,sBDiJ0B,CC7I7B,kBALG,2BAUH,CALA,QACG,0BAA2B,CAG3B,SACH,CCxBA,aAYG,uBAA6B,CAX7B,qDAAsD,CACtD,wBAAyB,CACzB,gBAAiB,CAiBjB,cAAe,CAdf,4BAA6B,CAG7B,kBAAmB,CAJnB,iBAAkB,CAclB,qBAEH,CAEA,OACG,yBACH,CAEA,8CACG,2DACH,CAEA,QAKG,sDAAuD,CACvD,gBAAiB,CACjB,8BAA+B,CAL/B,yBAA0B,CAE1B,6BAA8B,CAD9B,4BAA6B,CAM7B,SAAU,CAEV,iBAAkB,CAClB,qBAAsB,CAXtB,wBAYH,CAEA,uCACG,0DACH,CAEA,iGAGG,yDAA0D,CAD1D,oBAEH,CAEA,SAKG,wBAA6B,CAJ7B,WAAY,CAEZ,WAAY,CACZ,SAAU,CAFV,UAIH,CAEA,WAEG,yBAA0B,CAG1B,eAAgB,CAChB,kBACH,CAKA,yBAGG,kBAAmB,CAFnB,mBAAoB,CACpB,WAEH,CAEA,0BACG,8BACH,CAEA,2BACG,qBACH,CAEA,4BACG,iEACH,CAKA,eACG,mCAAoC,CACpC,sBACH,CCxGA,aAMG,YAAa,CAGb,cAAe,CAFf,mCAAqC,CACrC,gCAAkC,CAJlC,WAAY,CAHZ,QAAS,CACT,WAAY,CACZ,UAOH,CAEA,eAMG,aAAc,CAFd,mCAAoC,CAFpC,kBAAmB,CAOnB,eAAgB,CARhB,SAAU,CAIV,iBAAkB,CAKlB,kBACH,CCzBA,sBACG,qBACH,CAEA,iBACG,sBACH,CAEA,kBACG,aACH,CAEA,2BAEG,iCAAkC,CAClC,UACH,CAEA,6BAEG,wBAA4B,CAC5B,UACH,CAEA,kBACG,uBAAwB,CACxB,UACH,CAGA,aACG,wBAAmC,CACnC,UACH,CAEA,aACG,uBAAwB,CACxB,UACH,CAEA,oBACG,wBAAmC,CACnC,UACH,CAGA,uCAEG,wBACH,CClDA,WAEG,YAAa,CADb,uBAAmB,CAAnB,kBAEH,CAEA,cAGG,eAAgB,CAFhB,gBAAiB,CACjB,YAAa,CCJb,0BAAsB,CAAtB,qBDMH,CCFA,wBACG,8BACH,CAEA,8BACG,iCAEG,6BAAyC,CADzC,oBAEH,CACH,CAEA,kCACG,iCAEG,6BAAyC,CADzC,oBAEH,CACH,CAGA,6BACG,wBACH,CAEA,4BACG,wBACH,CAGA,uBACG,UACH,CAEA,sCACG,wBACH,CAEA,qCACG,wBACH,CC3CA,cAOG,8BAA+B,CAG/B,cAAkB,CAClB,4CAA6C,CAF7C,iBAGH,CAGA,cACG,iBAAkB,CAClB,kBAAmB,CACnB,gBAAiB,CAKjB,4CAA8C,CAI9C,qCAAsC,CADtC,wDAAyD,CANzD,2BAQH,CAEA,iBAQG,yBAA0B,CAN1B,UAAW,CAGX,mBAAoB,CAFpB,QAAS,CAFT,iBAAkB,CAGlB,iBAAkB,CAGlB,wBAEH,CAEA,8BACG,uBACG,cAAqB,CACrB,6CACH,CAEA,uBACG,cAAqB,CACrB,4BACH,CAEA,0BACG,SAAa,CACb,OACH,CACH,CAEA,kCACG,wBACG,cAAqB,CACrB,6CACH,CAEA,wBACG,cAAqB,CACrB,4BACH,CAEA,2BACG,SAAa,CACb,OACH,CACH,CAGA,eAEG,mBAAoB,CADpB,wBAgBH,CAEA,sBAEG,WAAkB,CAMlB,yBAA0B,CAJ1B,eAAgB,CAHhB,iBAAkB,CASlB,OAAQ,CALR,kBAAmB,CAFnB,wBAAiB,CAAjB,gBAAiB,CAMjB,UAEH,CAIA,oCACG,SACH,CAGA,gBACG,gBACH,CAEA,wBACG,wBACH,CAEA,qBACG,mBACH,CAEA,sBACG,gBACH,CCnIA,iBAGI,wBAAyB,CADzB,qBAAsB,CADtB,uBAAwB,CAGxB,YACJ,CAEA,mBACI,QAAS,CACT,oBAAqB,CACrB,qBACJ,CCVA,MACG,YAAa,CACb,mCACH,CAEA,cAEG,wCAAyC,CADzC,WAEH,CAEA,gBAEG,wBAAiC,CADjC,mBAEH,CAEA,sBACG,wBACH,CAEA,uBACG,wBAAiC,CACjC,kBACH,CCtBA,aAmBG,oBAAqB,CAXrB,qBAAsB,CAStB,kBAAmB,CADnB,kCAAmC,CAdnC,UAAW,CASX,YAAa,CAGb,qBAAsB,CADtB,6BAA8B,CAP9B,2BAA4B,CAM5B,mBAAyB,CAXzB,cAAe,CAEf,SAAU,CAEV,sBAAkB,CAAlB,iBAAkB,CAelB,oBAAqB,CAXrB,SAYH,CAGA,4BApBG,uBAAmB,CAAnB,kBAuBH,CAHA,eAEG,cACH,CAEA,oBACG,cACH,CAEA,eACG,eACH,CAIA,kBACG,6BACH,CAEA,qBACG,0BACH,CAEA,mBACG,uBACH,CCjDA,cAIG,sBAAuB,CACvB,YAAa,CAFb,YAAa,CAFb,cAAe,CACf,WAAY,CAIZ,SACH,CAEA,cAiBG,oBAAqB,CAFrB,oBAAqB,CADrB,iBAAkB,CATlB,wCAAyC,CAIzC,mBAAoB,CAGpB,qBAAsB,CAVtB,uBAAmB,CAAnB,kBAAmB,CAcnB,sBAAuB,CANvB,aAAc,CACd,YAAa,CAVb,iBAAkB,CAYlB,iBAAkB,CAVlB,sBAAkB,CAAlB,iBAAkB,CAelB,oBAAqB,CAZrB,SAaH,CAEA,oBACG,gBACH,CAEA,qBACG,gBAAiB,CACjB,UACH,CAEA,sBAEG,kBAAmB,CADnB,YAEH,CAEA,6BACG,UACH,CC9CA,eAIG,QAAS,CAHT,uBAAwB,CACxB,iBAAkB,CAGlB,OAAQ,CAFR,KAGH,CAEA,mBACG,YACH,CAEA,UACG,4BACH,CAEA,wBACG,MACG,sBACH,CAEA,QACG,wBACH,CAEA,QACG,uBACH,CACH,CAEA,8CACG,+BACG,uCACH,CAEA,yBACG,+BACG,cACH,CAEA,yBACG,uCACH,CACH,CACH,CAEA,uCACG,+BACG,qCACH,CAEA,yBACG,+BACG,cACH,CAEA,yBACG,qCACH,CACH,CACH,CC5DA,aAIG,2BAA4B,CAH5B,4BAA6B,CAI7B,uBAAmB,CAAnB,kBAAmB,CAFnB,cAAe,CADf,eAAgB,CAIhB,sBAAkB,CAAlB,iBACH","sources":["../node_modules/sanitize.css/sanitize.css","index.css","App.css","Elems/Main.css","Elems/MainElems/Sudoku.css","Elems/MainElems/Candidates.css","Elems/MainElems/Candidate.css","Elems/Aside.css","Elems/AsideElems/StrategyList.css","Elems/AsideElems/StrategyItem.css","Elems/AsideElems/StrategyDetails.css","Elems/AsideElems/Tabs.css","Elems/NoticeElems/AlertNotice.css","Elems/NoticeElems/PromptWindow.css","Elems/GithubCorner.css","ErrorNotice.css"],"sourcesContent":["/* Document\n * ========================================================================== */\n\n/**\n * 1. Add border box sizing in all browsers (opinionated).\n * 2. Backgrounds do not repeat by default (opinionated).\n */\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n background-repeat: no-repeat; /* 2 */\n}\n\n/**\n * 1. Add text decoration inheritance in all browsers (opinionated).\n * 2. Add vertical alignment inheritance in all browsers (opinionated).\n */\n\n::before,\n::after {\n text-decoration: inherit; /* 1 */\n vertical-align: inherit; /* 2 */\n}\n\n/**\n * 1. Use the default cursor in all browsers (opinionated).\n * 2. Change the line height in all browsers (opinionated).\n * 3. Breaks words to prevent overflow in all browsers (opinionated).\n * 4. Use a 4-space tab width in all browsers (opinionated).\n * 5. Remove the grey highlight on links in iOS (opinionated).\n * 6. Prevent adjustments of font size after orientation changes in iOS.\n */\n\n:where(:root) {\n cursor: default; /* 1 */\n line-height: 1.5; /* 2 */\n overflow-wrap: break-word; /* 3 */\n -moz-tab-size: 4; /* 4 */\n tab-size: 4; /* 4 */\n -webkit-tap-highlight-color: transparent; /* 5 */\n -webkit-text-size-adjust: 100%; /* 6 */\n}\n\n/* Sections\n * ========================================================================== */\n\n/**\n * Remove the margin in all browsers (opinionated).\n */\n\n:where(body) {\n margin: 0;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Edge, Firefox, and Safari.\n */\n\n:where(h1) {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n/* Grouping content\n * ========================================================================== */\n\n/**\n * Remove the margin on nested lists in Chrome, Edge, and Safari.\n */\n\n:where(dl, ol, ul) :where(dl, ol, ul) {\n margin: 0;\n}\n\n/**\n * 1. Correct the inheritance of border color in Firefox.\n * 2. Add the correct box sizing in Firefox.\n */\n\n:where(hr) {\n color: inherit; /* 1 */\n height: 0; /* 2 */\n}\n\n/**\n * Remove the list style on navigation lists in all browsers (opinionated).\n */\n\n:where(nav) :where(ol, ul) {\n list-style-type: none;\n padding: 0;\n}\n\n/**\n * Prevent VoiceOver from ignoring list semantics in Safari (opinionated).\n */\n\n:where(nav li)::before {\n content: \"\\200B\";\n float: left;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n * 3. Prevent overflow of the container in all browsers (opinionated).\n */\n\n:where(pre) {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n overflow: auto; /* 3 */\n}\n\n/* Text-level semantics\n * ========================================================================== */\n\n/**\n * Add the correct text decoration in Safari.\n */\n\n:where(abbr[title]) {\n text-decoration: underline;\n text-decoration: underline dotted;\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\n:where(b, strong) {\n font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\n:where(code, kbd, samp) {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\n:where(small) {\n font-size: 80%;\n}\n\n/* Embedded content\n * ========================================================================== */\n\n/*\n * Change the alignment on media elements in all browsers (opinionated).\n */\n\n:where(audio, canvas, iframe, img, svg, video) {\n vertical-align: middle;\n}\n\n/**\n * Remove the border on iframes in all browsers (opinionated).\n */\n\n:where(iframe) {\n border-style: none;\n}\n\n/**\n * Change the fill color to match the text color in all browsers (opinionated).\n */\n\n:where(svg:not([fill])) {\n fill: currentColor;\n}\n\n/* Tabular data\n * ========================================================================== */\n\n/**\n * 1. Collapse border spacing in all browsers (opinionated).\n * 2. Correct table border color inheritance in all Chrome, Edge, and Safari.\n * 3. Remove text indentation from table contents in Chrome, Edge, and Safari.\n */\n\n:where(table) {\n border-collapse: collapse; /* 1 */\n border-color: inherit; /* 2 */\n text-indent: 0; /* 3 */\n}\n\n/* Forms\n * ========================================================================== */\n\n/**\n * Remove the margin on controls in Safari.\n */\n\n:where(button, input, select) {\n margin: 0;\n}\n\n/**\n * Correct the inability to style buttons in iOS and Safari.\n */\n\n:where(button, [type=\"button\" i], [type=\"reset\" i], [type=\"submit\" i]) {\n -webkit-appearance: button;\n}\n\n/**\n * Change the inconsistent appearance in all browsers (opinionated).\n */\n\n:where(fieldset) {\n border: 1px solid #a0a0a0;\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Edge, and Firefox.\n */\n\n:where(progress) {\n vertical-align: baseline;\n}\n\n/**\n * 1. Remove the margin in Firefox and Safari.\n * 3. Change the resize direction in all browsers (opinionated).\n */\n\n:where(textarea) {\n margin: 0; /* 1 */\n resize: vertical; /* 3 */\n}\n\n/**\n * 1. Correct the odd appearance in Chrome, Edge, and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n:where([type=\"search\" i]) {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Safari.\n */\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/**\n * Correct the text style of placeholders in Chrome, Edge, and Safari.\n */\n\n::-webkit-input-placeholder {\n color: inherit;\n opacity: 0.54;\n}\n\n/**\n * Remove the inner padding in Chrome, Edge, and Safari on macOS.\n */\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style upload buttons in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/* Interactive\n * ========================================================================== */\n\n/*\n * Add the correct styles in Safari.\n */\n\n:where(dialog) {\n background-color: white;\n border: solid;\n color: black;\n height: -moz-fit-content;\n height: fit-content;\n left: 0;\n margin: auto;\n padding: 1em;\n position: absolute;\n right: 0;\n width: -moz-fit-content;\n width: fit-content;\n}\n\n:where(dialog:not([open])) {\n display: none;\n}\n\n/*\n * Add the correct display in Safari.\n */\n\n:where(details > summary:first-of-type) {\n display: list-item;\n}\n\n/* Accessibility\n * ========================================================================== */\n\n/**\n * Change the cursor on busy elements in all browsers (opinionated).\n */\n\n:where([aria-busy=\"true\" i]) {\n cursor: progress;\n}\n\n/*\n * Change the cursor on control elements in all browsers (opinionated).\n */\n\n:where([aria-controls]) {\n cursor: pointer;\n}\n\n/*\n * Change the cursor on disabled, not-editable, or otherwise\n * inoperable elements in all browsers (opinionated).\n */\n\n:where([aria-disabled=\"true\" i], [disabled]) {\n cursor: not-allowed;\n}\n\n/*\n * Change the display on visually hidden accessible elements\n * in all browsers (opinionated).\n */\n\n:where([aria-hidden=\"false\" i][hidden]) {\n display: initial;\n}\n\n:where([aria-hidden=\"false\" i][hidden]:not(:focus)) {\n clip: rect(0, 0, 0, 0);\n position: absolute;\n}\n","@import url(sanitize.css);\r\n\r\nbody {\r\n margin: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n}\r\n\r\n.monospace,\r\ncode {\r\n font-family: var(--monospace);\r\n}\r\n\r\n:root {\r\n --monospace: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;\r\n\r\n /* Make font-size relative to screen size */\r\n font-size: calc(0.5em + 1vmin);\r\n\r\n /* Only supports dark-mode for now */\r\n color-scheme: dark;\r\n}\r\n\r\n@media print {\r\n :root {\r\n --text-color: black;\r\n --background-color: white;\r\n --link-color: rgb(0, 0, 238);\r\n --link-active-color: red;\r\n --link-visited-color: rgb(85, 26, 139);\r\n }\r\n}\r\n","\r\n/* root variables */\r\n:root {\r\n --relative-pixel: calc(1ch / 25);\r\n\r\n /* Border variables to make it look like a sudoku */\r\n --regular-border: calc(2 * var(--relative-pixel));\r\n --larger-border: calc(8 * var(--regular-border));\r\n --large-border: calc(var(--larger-border) / 3);\r\n\r\n --sudokucell-length: 4rem;\r\n --cell-fontsize: 2.5rem; /* fontsize of a solved digit */\r\n --candidate-fontsize: 0.9rem;\r\n\r\n /* colors */\r\n --background-color: #282c34; /* dark mode */\r\n --link-color: #61dafb;\r\n --link-visited-color: #d7a1e8;\r\n --link-active-color: tomato;\r\n --text-color: whitesmoke;\r\n --border-color: white;\r\n --maroon: maroon; /* rgb(139, 0, 0)? */\r\n --red: #ec3e31;\r\n --orange: orange;\r\n --blue-gray: #8cadf3;\r\n --lighter-green: #8ea;\r\n --light-green: #0c0;\r\n --green: green;\r\n --purple: rebeccapurple;\r\n}\r\n\r\n.App {\r\n background-color: var(--background-color);\r\n color: var(--text-color);\r\n\r\n /* For the github-corner */\r\n position: relative;\r\n\r\n min-height: 100vh;\r\n min-width: 100vw;\r\n width: fit-content;\r\n margin: 0;\r\n\r\n display: grid;\r\n gap: 1rem;\r\n align-content: space-between;\r\n /* default moved to min-width media querys */\r\n\r\n /* See app.main */\r\n --app-main-height: calc(\r\n calc( /* --sudoku-length */\r\n 9 * var(--sudokucell-length) +\r\n calc(7 * var(--regular-border) * 2) +\r\n calc(2 * var(--regular-border) * 1) +\r\n calc(2 * 8 * var(--regular-border) / 3)\r\n )\r\n );\r\n}\r\n\r\n/* width >= 900px, 2 column layout*/\r\n@media (min-width: 900px) {\r\n .App {\r\n grid-template-columns: 250fr 1432fr 150fr 1314fr 250fr; /* ~ (8%, 44%, 40%, 8%) */\r\n grid-template-rows: auto var(--app-main-height) 0.3fr; /* prev ~ (7%, 7%, 2rem, 72%, 7%) */\r\n gap: 0 0;\r\n grid-template-areas:\r\n \"Header Header Header Header Header\"\r\n \". Main . Aside .\"\r\n \". . . . .\";\r\n }\r\n\r\n /* Declares variable for consistency (see below) */\r\n .App-aside {\r\n --font-size: 1rem;\r\n }\r\n}\r\n\r\n/* width <= 900px, 1 column layout */\r\n@media (max-width: 900px) {\r\n .App {\r\n grid-template-columns: 250fr 2745fr 250fr; /* ~ (8%, 84%, 8%) */\r\n grid-template-rows: auto var(--app-main-height) auto;\r\n grid-template-areas:\r\n \". Header .\"\r\n \". Main .\"\r\n \". Aside .\";\r\n\r\n justify-content: center;\r\n }\r\n\r\n /**\r\n * Bigger text (aside)\r\n * Variable exists because of the strategyItem shenanigans\r\n * Margin: Gap + Allow scrolling past the aside in one column mode\r\n */\r\n .App-aside {\r\n --font-size: calc(1rem + 1vh);\r\n font-size: var(--font-size);\r\n margin: 1.5rem 0 25vh 0;\r\n }\r\n}\r\n\r\n/*\r\n@keyframes App-logo-spin {\r\n from {\r\n transform: rotate(0deg);\r\n }\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n.App-logo {\r\n height: 40vmin;\r\n pointer-events: none;\r\n}\r\n\r\n@media (prefers-reduced-motion: no-preference) {\r\n .App-logo {\r\n animation: App-logo-spin infinite 20s linear;\r\n }\r\n}\r\n*/\r\n\r\n.App-header {\r\n grid-area: Header;\r\n padding: 2vh 0;\r\n height: fit-content;\r\n font-size: calc(10px + 2vmin);\r\n}\r\n\r\n.App-header > h1.Title {\r\n display: inline;\r\n font-size: 1em;\r\n padding: 0 2vw;\r\n margin: 0;\r\n}\r\n\r\n.App-header > .Version {\r\n font-size: 0.7em;\r\n color: silver;\r\n}\r\n\r\n/* TODO: Fix layout when error */\r\n.App.error {\r\n border: 0.5rem solid var(--red);\r\n}\r\n\r\n.App.error > .App-header::before {\r\n content: \"Error! \";\r\n color: var(--red);\r\n}\r\n\r\n\r\n.App-link {\r\n color: var(--link-color);\r\n}\r\n\r\n.App-link:visited {\r\n color: var(--link-visited-color);\r\n}\r\n\r\n.App-link:active {\r\n color: var(--link-active-color);\r\n}\r\n\r\n.App-aside { grid-area: Aside; }\r\n.App-main { grid-area: Main; }\r\n\r\n\r\n\r\n",".App-main {\r\n /* variables (inherited by children) */\r\n --sudoku-length: calc(\r\n 9 * var(--sudokucell-length) +\r\n calc(7 * var(--regular-border) * 2) +\r\n calc(2 * var(--regular-border) * 1) +\r\n calc(2 * var(--large-border))\r\n ); /*\r\n Length = Row width (border doesn't count because of box-sizing: border-box)\r\n\r\n Row width = 9 cells\r\n 7 cells = width + 2regular-border\r\n 2 cells = width + 1regular-border + 1large-border\r\n\r\n total = 9 * width +\r\n (7 * 2 * regular) +\r\n (2 * 1 * regular) +\r\n (2 * large)\r\n */\r\n\r\n display: flex;\r\n height: var(--sudoku-length);\r\n justify-content: center;\r\n align-content: center;\r\n}\r\n\r\n.Sudoku {\r\n width: var(--sudoku-length);\r\n height: var(--sudoku-length);\r\n\r\n z-index: 3;\r\n}\r\n","\r\n/*\r\n References:\r\n [1]: https://stackoverflow.com/questions/4457506\r\n [2]: https://stackoverflow.com/questions/13667941\r\n*/\r\n\r\ntable.Sudoku {\r\n border: var(--larger-border) solid var(--border-color);\r\n border-collapse: collapse;\r\n border-spacing: 0;\r\n\r\n text-align: center;\r\n font-family: var(--monospace);\r\n\r\n /* [ref 1] - height and width are set in Main.css */\r\n table-layout: fixed;\r\n\r\n /* Also --cell-width */\r\n --cell-height: calc(100% / 9);\r\n\r\n /* The following variables are inherited by :root\r\n --cell-fontsize: 2.5rem;\r\n --candidate-fontsize: 0.9rem;\r\n */\r\n\r\n vertical-align: middle;\r\n cursor: pointer;\r\n}\r\n\r\ntr.Row {\r\n height: var(--cell-height);\r\n}\r\n\r\ntr.Row:nth-child(3n):not(:last-child) > td.Cell {\r\n border-bottom: var(--large-border) solid var(--border-color);\r\n}\r\n\r\ntd.Cell {\r\n width: var(--cell-height);\r\n height: var(--cell-height); /* part of trying @[ref 1] */\r\n max-width: var(--cell-height);\r\n max-height: var(--cell-height);\r\n border: var(--regular-border) solid var(--border-color);\r\n border-spacing: 0;\r\n font-size: var(--cell-fontsize);\r\n\r\n padding: 0;\r\n\r\n text-align: center;\r\n vertical-align: middle;\r\n}\r\n\r\ntd.Cell:nth-child(3n):not(:last-child) {\r\n border-right: var(--large-border) solid var(--border-color);\r\n}\r\n\r\n[dir=rtl] td.Cell:nth-child(3n):not(:last-child),\r\ntd.Cell:nth-child(3n):not(:last-child):dir(rtl) {\r\n border-right: initial;\r\n border-left: var(--large-border) solid var(--border-color);\r\n}\r\n\r\ndiv.Cell {\r\n border: none;\r\n width: 100%;\r\n height: 100%;\r\n padding: 0;\r\n background-color: transparent;\r\n}\r\n\r\ndiv.Cell > * {\r\n /* [ref 2] */\r\n height: var(--cell-height);\r\n\r\n /* & [ref 1] */\r\n overflow: hidden;\r\n text-overflow: clip;\r\n}\r\n\r\n/* This selector was made before I needed to use it\r\n * This is for vertically aligning the big digits\r\n */\r\ndiv.Cell > span.ugh.tables {\r\n display: inline-flex;\r\n height: 100%;\r\n align-items: center;\r\n}\r\n\r\ndiv.Cell[data-error=\"true\"] {\r\n background-color: var(--maroon);\r\n}\r\n\r\ndiv.Cell[data-active=\"true\"] {\r\n background-color: #555;\r\n}\r\n\r\ndiv.Cell[data-active=\"false\"] {\r\n background: repeating-linear-gradient(-45deg, #d35e5e, #44476b, #333);\r\n}\r\n\r\n/* Section moved to Candidates.css */\r\n\r\n/* For the lazy loaded props */\r\n.Cell > .Loading {\r\n font-size: var(--candidate-fontsize);\r\n text-overflow: ellipsis;\r\n}\r\n\r\n","\r\n/* Moved from Sudoku.css */\r\n\r\np.Candidates {\r\n margin: 0;\r\n padding: 2pt;\r\n width: 100%;\r\n height: 100%;\r\n\r\n display: grid;\r\n grid-template-columns: repeat(3, 1fr);\r\n grid-template-rows: repeat(3, 1fr);\r\n font-size: 1rem;\r\n}\r\n\r\nspan.Candidate {\r\n padding: 0;\r\n line-height: normal;\r\n\r\n font-size: var(--candidate-fontsize);\r\n text-align: center;\r\n color: inherit;\r\n\r\n /* [ref 1] */\r\n overflow: hidden;\r\n text-overflow: clip;\r\n}\r\n","\r\n.Candidate.eliminated {\r\n background-color: yellow;\r\n}\r\n\r\n.Candidate.added {\r\n background-color: green;\r\n}\r\n\r\n.Candidate.solved {\r\n color: lightgreen;\r\n}\r\n\r\n.Cell.blue,\r\n.Candidate.blue {\r\n background-color: var(--blue-gray);\r\n color: black;\r\n}\r\n\r\n.Cell.green,\r\n.Candidate.green {\r\n background-color: lightgreen;\r\n color: black;\r\n}\r\n\r\n.Candidate.orange {\r\n background-color: orange;\r\n color: black;\r\n}\r\n\r\n/* More like tan instead of orange, but at least my eyes don't hurt from the contrast */\r\n.Cell.orange {\r\n background-color: hsl(39, 80%, 70%); /* orange - 20 sat + 20 lig */\r\n color: black;\r\n}\r\n\r\n.Cell.salmon {\r\n background-color: salmon;\r\n color: black;\r\n}\r\n\r\n.Cell.orange.salmon {\r\n background-color: hsl(23, 87%, 71%);\r\n color: black;\r\n}\r\n\r\n/* Halfway between the styles in blue and green */\r\n.Cell.blue.green,\r\n.Candidate.blue.green {\r\n background-color: #8ecec2;\r\n}\r\n",".App-aside {\r\n height: fit-content;\r\n border: inset;\r\n}\r\n\r\n.StrategyList {\r\n margin-left: 1rem;\r\n margin-top: 0;\r\n margin-bottom: 0;\r\n}\r\n","\r\n/* Overflow */\r\n.StrategyList {\r\n min-width: fit-content;\r\n}\r\n\r\n/* The current strategy item */\r\n.StrategyItem.isCurrent {\r\n border-right: 5px solid rgb(183, 193, 194);\r\n}\r\n\r\n@supports selector(:dir(rtl)) {\r\n .StrategyItem.isCurrent:dir(rtl) {\r\n border-right: initial;\r\n border-left: 5px solid rgb(183, 193, 194);\r\n }\r\n}\r\n\r\n@supports not selector(:dir(rtl)) {\r\n .StrategyItem.isCurrent[dir=rtl] {\r\n border-right: initial;\r\n border-left: 5px solid rgb(183, 193, 194);\r\n }\r\n}\r\n\r\n/* Alternating colors - note that the markers are not in color */\r\n.StrategyItem:nth-child(odd) {\r\n background-color: #1f2329;\r\n}\r\n\r\n.StrategyItem:nth-child(even) {\r\n background-color: #31353f;\r\n}\r\n\r\n/* Disabled strategy item */\r\n.StrategyItem.disabled {\r\n color: rgb(170, 170, 170);\r\n}\r\n\r\n.StrategyItem.disabled:nth-child(odd) {\r\n background-color: #282b31;\r\n}\r\n\r\n.StrategyItem.disabled:nth-child(even) {\r\n background-color: #31353c;\r\n}\r\n","\r\n /* Checkbox positioning */\r\n.StrategyList {\r\n /*\r\n These variables were moved from .StrategyItem\r\n\r\n The checkbox needs to be --item-height\r\n This impacts the strategy list, since the padding-left must account for the checkbox\r\n */\r\n --item-height: var(--font-size); /* see App.css */\r\n\r\n position: relative;\r\n max-width: initial;\r\n padding-left: calc(2rem + var(--item-height));\r\n}\r\n\r\n/* Sidenote discovery: Google Chrome doesn't know that 0 is a valid when using max() */\r\n.StrategyItem {\r\n --label-width: 80%; /* For status */\r\n --status-width: 20%; /* For status */\r\n --padding: 1.5rem;\r\n\r\n padding-left: var(--padding);\r\n\r\n /* But this is for list item positioning */\r\n line-height: calc(var(--item-height) + 0.1rem);\r\n\r\n /* Making room for status */\r\n min-width: calc(var(--label-width) + var(--status-width));\r\n max-width: calc(100% - var(--padding));\r\n}\r\n\r\n.StrategyToggler {\r\n position: absolute;\r\n left: -1rem;\r\n margin: 0; /* Now sanitize.css works! */\r\n text-align: center;\r\n line-height: inherit;\r\n\r\n width: var(--item-height);\r\n height: var(--item-height);\r\n}\r\n\r\n@supports selector(:dir(rtl)) {\r\n .StrategyList:dir(rtl) {\r\n padding-left: initial;\r\n padding-right: calc(2rem + var(--item-height));\r\n }\r\n\r\n .StrategyItem:dir(rtl) {\r\n padding-left: initial;\r\n padding-right: var(--padding);\r\n }\r\n\r\n .StrategyToggler:dir(rtl) {\r\n left: initial;\r\n right: 0;\r\n }\r\n}\r\n\r\n@supports not selector(:dir(rtl)) {\r\n [dir=rtl] .StrategyList {\r\n padding-left: initial;\r\n padding-right: calc(2rem + var(--item-height));\r\n }\r\n\r\n [dir=rtl] .StrategyItem {\r\n padding-left: initial;\r\n padding-right: var(--padding);\r\n }\r\n\r\n [dir=rtl] .StrategyToggler {\r\n left: initial;\r\n right: 0;\r\n }\r\n}\r\n\r\n/* Label structure: Name Status */\r\n.StrategyLabel {\r\n width: var(--label-width);\r\n display: inline-grid; /* Still inline but behaves like a block element */\r\n\r\n /*\r\n Right now this doesn't do anything, (besides display)\r\n since the StrategyLabel doesn't contain a tooltip anymore.\r\n\r\n This would make more sense in a parent \"StrategyItemContent\" span\r\n As of right now, the \"StrategyStatus\" are aligned by the constant width.\r\n\r\n align-content: center;\r\n display: inline-grid;\r\n grid-template-columns: 3fr 1fr;\r\n grid-template-areas:\r\n 'name status';\r\n */\r\n}\r\n\r\n.StrategyTogglerLabel {\r\n position: absolute;\r\n color: transparent;\r\n user-select: none;\r\n overflow: hidden;\r\n text-overflow: clip;\r\n\r\n /* Tap size */\r\n height: var(--item-height);\r\n width: 100%;\r\n right: 0;\r\n}\r\n\r\n/* If there's a link don't block it */\r\n/* (a[href] + label) selects label */\r\na[href] + label > .StrategyTogglerLabel {\r\n width: 20%;\r\n}\r\n\r\n/* Result */\r\n.StrategyResult {\r\n grid-area: status;\r\n}\r\n\r\n.StrategyResult.success {\r\n color: var(--light-green);\r\n}\r\n\r\n.StrategyResult.fail {\r\n color: var(--orange);\r\n}\r\n\r\n.StrategyResult.error {\r\n color: var(--red);\r\n}\r\n",".StrategyDetails {\r\n margin-inline-start: 2px;\r\n margin-inline-end: 2px;\r\n border: 1px solid #a0a0a0;\r\n padding: 0.5em;\r\n}\r\n\r\n.StrategyDetails > p {\r\n margin: 0;\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n}\r\n","\r\n.Tabs {\r\n display: grid;\r\n grid-template-columns: repeat(2, 1fr);\r\n}\r\n\r\n.Tab.selected {\r\n border: none;\r\n background-color: var(--background-color);\r\n}\r\n\r\n.Tab.unselected {\r\n border-style: outset;\r\n background-color: rgb(80, 80, 80);\r\n}\r\n\r\n.Tab.unselected:hover {\r\n background-color: rgb(118, 118, 118);\r\n}\r\n\r\n.Tab.unselected:active {\r\n background-color: rgb(55, 55, 55);\r\n border-style: inset;\r\n}\r\n","\r\n.AlertNotice {\r\n position: fixed;\r\n bottom: 2vh;\r\n right: 2vw;\r\n height: fit-content;\r\n width: fit-content;\r\n max-width: calc(100% - 4rem);\r\n\r\n background-color: #444;\r\n z-index: 4;\r\n\r\n display: flex;\r\n padding: 0 1rem 1rem 1rem;\r\n justify-content: space-between;\r\n flex-direction: column;\r\n\r\n border-width: var(--regular-border);\r\n border-style: solid;\r\n\r\n word-wrap: break-word;\r\n word-break: break-all;\r\n}\r\n\r\n/** Positioning */\r\n.AlertNotice > p {\r\n height: fit-content;\r\n font-size: 1rem;\r\n}\r\n\r\n.AlertNotice > button {\r\n font-size: 2rem;\r\n}\r\n\r\n.AlertNotice > * {\r\n position: sticky;\r\n}\r\n\r\n\r\n/** Types of alert notices */\r\n.AlertNotice.info {\r\n border-color: var(--blue-gray);\r\n}\r\n\r\n.AlertNotice.warning {\r\n border-color: var(--orange);\r\n}\r\n\r\n.AlertNotice.error {\r\n border-color: var(--red);\r\n}\r\n","\r\n.PromptWindow {\r\n position: fixed; /** prevent escape! Note that aria-modal is implicitly true */\r\n width: 100vw;\r\n height: 100vh;\r\n background-color: #7777;\r\n display: flex;\r\n z-index: 4;\r\n}\r\n\r\n.PromptNotice {\r\n position: relative;\r\n height: fit-content;\r\n width: fit-content;\r\n\r\n background-color: var(--background-color);\r\n z-index: 4;\r\n\r\n /* Everything here is just for centering */\r\n display: inline flex;\r\n margin: 0 auto;\r\n padding: 1rem;\r\n flex-direction: column;\r\n text-align: center;\r\n align-self: center;\r\n align-content: center;\r\n justify-content: center;\r\n word-wrap: break-word;\r\n word-break: break-all;\r\n}\r\n\r\n.PromptNotice > label {\r\n font-size: 1.5rem;\r\n}\r\n\r\n.PromptNotice > button {\r\n font-size: 1.5rem;\r\n width: 100%;\r\n}\r\n\r\n.PromptNotice > label > p {\r\n margin-top: 0;\r\n margin-bottom: 1rem;\r\n}\r\n\r\n.PromptNotice > label > textarea {\r\n width: 100%;\r\n}\r\n\r\n\r\n","\r\n.github-corner {\r\n color: var(--text-color);\r\n position: absolute;\r\n top: 0;\r\n border: 0;\r\n right: 0;\r\n}\r\n\r\n.github-corner > svg {\r\n fill: #151513;\r\n}\r\n\r\n.octo-arm {\r\n transform-origin: 130px 106px;\r\n}\r\n\r\n@keyframes octocat-wave {\r\n 0%, 100% {\r\n transform: rotate(0deg);\r\n }\r\n\r\n 20%, 60% {\r\n transform: rotate(-25deg);\r\n }\r\n\r\n 40%, 80% {\r\n transform: rotate(10deg);\r\n }\r\n}\r\n\r\n@media (prefers-reduced-motion: no-preference) {\r\n .github-corner:hover .octo-arm {\r\n animation: octocat-wave 560ms ease-in-out;\r\n }\r\n\r\n @media (max-width: 500px) {\r\n .github-corner:hover .octo-arm {\r\n animation: none;\r\n }\r\n\r\n .github-corner .octo-arm {\r\n animation: octocat-wave 560ms ease-in-out;\r\n }\r\n }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .github-corner:hover .octo-arm {\r\n animation: octocat-wave 7s ease-in-out;\r\n }\r\n\r\n @media (max-width: 500px) {\r\n .github-corner:hover .octo-arm {\r\n animation: none;\r\n }\r\n\r\n .github-corner .octo-arm {\r\n animation: octocat-wave 7s ease-in-out;\r\n }\r\n }\r\n}\r\n","\r\n.ErrorNotice {\r\n font-family: var(--monospace);\r\n white-space: pre;\r\n padding: 1.5rem;\r\n border: 3px solid var(--red);\r\n height: fit-content;\r\n width: fit-content;\r\n}\r\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/static/js/main.202c3475.js b/static/js/main.202c3475.js new file mode 100644 index 00000000..10a294b3 --- /dev/null +++ b/static/js/main.202c3475.js @@ -0,0 +1,3 @@ +/*! For license information please see main.202c3475.js.LICENSE.txt */ +(()=>{"use strict";var e={123:e=>{var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function o(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(o){return!1}}()?Object.assign:function(e,l){for(var s,i,a=o(e),u=1;u{var r=n(43),o=n(123),l=n(853);function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n
\r\n )\r\n }\r\n}\r\n","import React from 'react';\r\nimport ExternalLink from '../ExternalLink';\r\n\r\nexport type StrategyLabelProps = Readonly<{\r\n name: string\r\n href?: string\r\n id?: string\r\n}>\r\n\r\n/**\r\n * The text {labelling} (naming) [for] a strategy; inside such StrategyItem.\r\n */\r\nexport default class StrategyLabel extends React.PureComponent {\r\n render() {\r\n if (this.props.href) {\r\n return (\r\n \r\n {this.props.name}\r\n \r\n )\r\n }\r\n\r\n return {this.props.name}\r\n }\r\n}\r\n","\r\nimport React from 'react';\r\n\r\ntype StrategyTogglerProps = Readonly<{\r\n callback: React.ChangeEventHandler\r\n id: string\r\n}>\r\n\r\ntype StrategyTogglerState = Readonly<{\r\n checked: boolean\r\n}>\r\n\r\n/**\r\n * Turns a strategy off or on\r\n */\r\nexport default class StrategyToggler extends React.Component {\r\n constructor(props: StrategyTogglerProps) {\r\n super(props)\r\n\r\n this.state = {\r\n checked: true\r\n }\r\n\r\n this.callback = this.callback.bind(this)\r\n }\r\n\r\n render() {\r\n // Apparently there shouldn't be an aria-checked\r\n return (\r\n \r\n )\r\n }\r\n\r\n callback(_event: React.ChangeEvent) {\r\n this.setState(state => ({ checked: !state.checked }))\r\n this.props.callback(_event)\r\n }\r\n}\r\n","import { SudokuDigits } from \"../Types\";\r\nimport PureSudoku from \"./Spaces/PureSudoku\";\r\nimport { CellID } from \"./Utils\";\r\n\r\nexport const SuccessError = -1\r\nexport type StrategyResult = Readonly<{\r\n success: true,\r\n successcount: number,\r\n message?: string\r\n} | {\r\n success: false,\r\n successcount?: number,\r\n message?: string\r\n}>\r\nexport type StrategyError = Readonly<{\r\n success: false,\r\n successcount: typeof SuccessError,\r\n message?: string\r\n}>\r\n\r\nexport type PureStrategy = (arg: PureSudoku) => StrategyResult\r\ntype StatefulStrategy = (arg: PureSudoku, memory: StrategyMemory[number]) => StrategyResult\r\n// eslint-disable-next-line @typescript-eslint/ban-types -- `Function` is only used for the `name` property\r\nexport type Strategy = Function & (PureStrategy | StatefulStrategy)\r\n\r\n// Group types\r\nexport type CellInfo = {\r\n candidates: SudokuDigits[]\r\n position: CellID\r\n}\r\n\r\nexport type CellGroup = CellInfo[]\r\n\r\n\r\n// Information strategies might want to know\r\nexport class StrategyMemory extends Array {\r\n public [0] = { solved: 0 }\r\n}\r\n","import React from 'react';\r\nimport { SuccessError } from '../../Api/Types';\r\n\r\nexport type StrategyStatusProps = Readonly<{\r\n success: null\r\n successcount: null | number\r\n}> | Readonly<{\r\n success: boolean\r\n successcount: number\r\n}>\r\n\r\n/**\r\n * How did trying the strategy go?\r\n */\r\nexport default class StrategyStatus extends React.Component {\r\n render() {\r\n const resultText =\r\n this.props.success === null\r\n ? '-'\r\n : this.props.success\r\n ? this.props.successcount === 1\r\n ? 'Yes'\r\n : this.props.successcount\r\n : this.props.successcount === SuccessError\r\n ? 'Error!'\r\n : 'No';\r\n\r\n const cssClass =\r\n this.props.success === null\r\n ? 'null'\r\n : this.props.success\r\n ? 'success'\r\n : 'fail';\r\n\r\n return (\r\n \r\n {resultText}\r\n \r\n )\r\n }\r\n}\r\n","import React from 'react';\r\n\r\nexport type StrategyTogglerLabelProps = Readonly<{\r\n name: string\r\n id?: string\r\n}>\r\n\r\n/**\r\n * Same as {@link StrategyLabel}, but this time labelling the {@link StrategyToggler}\r\n */\r\nexport default class StrategyTogglerLabel extends React.PureComponent {\r\n render() {\r\n return (\r\n \r\n {`Toggle ${this.props.name}`}\r\n \r\n )\r\n }\r\n}\r\n","import './StrategyItem.css'\r\nimport React from 'react';\r\nimport StrategyLabel, { StrategyLabelProps } from './StrategyLabel';\r\nimport StrategyToggler from './StrategyToggler';\r\nimport StrategyStatus, { StrategyStatusProps } from './StrategyStatus';\r\nimport Solver from '../../Api/Solver';\r\nimport StrategyTogglerLabel from './StrategyTogglerLabel';\r\n\r\nexport type StrategyItemProps = StrategyLabelProps & Readonly<{\r\n solver: Solver\r\n index: number\r\n required?: true | 'true'\r\n}>\r\n\r\nexport type StrategyItemState = StrategyStatusProps & Readonly<{\r\n disabled: boolean\r\n isCurrentStrategy: boolean\r\n}>\r\n\r\ntype StrategyResult = {\r\n success: boolean\r\n successcount: number | null\r\n}\r\n\r\n/**\r\n * The strategy element\r\n *\r\n * Passes props to StrategyLabel\r\n */\r\nexport default class StrategyItem extends React.Component {\r\n id: `strategy-${string}`;\r\n labelId: `label-for-strategy-${string}`;\r\n togglerId?: `strategy-toggler-${string}`;\r\n constructor(props: StrategyItemProps) {\r\n super(props)\r\n\r\n const name = this.props.name.replaceAll(' ', '-')\r\n this.id = `strategy-${name}` as const\r\n this.labelId = `label-for-strategy-${name}` as const\r\n if (this.props.required === undefined) {\r\n this.togglerId = `strategy-toggler-${name}` as const\r\n }\r\n\r\n this.state = {\r\n success: null,\r\n successcount: null,\r\n\r\n disabled: false,\r\n isCurrentStrategy: false\r\n }\r\n\r\n this.reset = this.reset.bind(this)\r\n this.toggle = this.toggle.bind(this)\r\n this.whenStepFinished = this.whenStepFinished.bind(this)\r\n }\r\n\r\n componentDidMount() {\r\n this.props.solver.eventRegistry.addEventListener('step finish', this.whenStepFinished)\r\n this.props.solver.eventRegistry.addEventListener('new turn', this.reset)\r\n }\r\n\r\n componentWillUnmount() {\r\n this.props.solver.eventRegistry.removeEventListener('step finish', this.whenStepFinished)\r\n this.props.solver.eventRegistry.removeEventListener('new turn', this.reset)\r\n }\r\n\r\n whenStepFinished({success, successcount}: StrategyResult, index: number) {\r\n if (index === this.props.index) {\r\n this.setState({ success, successcount, isCurrentStrategy: true })\r\n } else {\r\n this.setState({ isCurrentStrategy: false })\r\n }\r\n }\r\n\r\n render() {\r\n /**\r\n * a11y considerations:\r\n *\r\n * I want the checkbox to be togglable by clicking any part of the text.\r\n *\r\n * But the text itself isn't a good label;\r\n * A better label would be \"toggle strategyName\" instead of \"strategyName\"\r\n *\r\n * Also, \"strategyName\" should label the
  • rather than the checkbox\r\n */\r\n let thisClass = 'StrategyItem'\r\n if (this.state.disabled) {\r\n thisClass += ' disabled'\r\n }\r\n if (this.state.isCurrentStrategy) {\r\n thisClass += ' isCurrent'\r\n }\r\n\r\n const togglerPart = this.props.required ? <> : (\r\n \r\n )\r\n\r\n return (\r\n
  • \r\n \r\n {togglerPart}\r\n \r\n
  • \r\n )\r\n\r\n // StrategyLabel goes before StrategyToggler because\r\n // it makes sense a11y wise to put the text first\r\n\r\n // And also because the site supports both ltr and rtl (hopefully)\r\n }\r\n\r\n toggle(_event: React.ChangeEvent) {\r\n this.setState(state => ({ disabled: !state.disabled }), () => {\r\n this.props.solver.disabled[this.props.index] = this.state.disabled\r\n })\r\n }\r\n\r\n reset() {\r\n this.setState({ success: null, successcount: null })\r\n }\r\n}\r\n","\r\nimport './StrategyList.css'\r\nimport React from 'react';\r\nimport Solver from '../../Api/Solver';\r\nimport { GuaranteedConstructCallback } from '../../Types';\r\nimport StrategyItem from './StrategyItem';\r\n\r\ntype StrategyListProps = Readonly<{\r\n solver: Solver\r\n}> & GuaranteedConstructCallback\r\n\r\n/**\r\n * A list of strategies\r\n */\r\nexport default class StrategyList extends React.Component {\r\n constructor(props: StrategyListProps) {\r\n super(props)\r\n this.props.whenConstruct()\r\n }\r\n\r\n render() {\r\n let index = 0\r\n\r\n const getRepeatedProps = () => {\r\n return {\r\n solver: this.props.solver,\r\n index: index++\r\n }\r\n }\r\n\r\n return (\r\n
      \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
    \r\n )\r\n }\r\n}\r\n","\r\nimport './StrategyDetails.css'\r\nimport React from \"react\"\r\nimport Solver from \"../../Api/Solver\"\r\nimport { StrategyResult } from \"../../Api/Types\"\r\n\r\ntype StrategyDetailsProps = Readonly<{\r\n solver: Solver\r\n}>\r\n\r\ntype StrategyDetailsState = Readonly<{\r\n text: string\r\n}>\r\n\r\nexport default class StrategyDetails extends React.Component {\r\n constructor(props: StrategyDetailsProps) {\r\n super(props)\r\n\r\n this.state = {\r\n text: \"\"\r\n }\r\n\r\n this.reset = this.reset.bind(this)\r\n this.whenStepFinished = this.whenStepFinished.bind(this)\r\n }\r\n\r\n componentDidMount() {\r\n this.props.solver.eventRegistry.addEventListener('step finish', this.whenStepFinished)\r\n this.props.solver.eventRegistry.addEventListener('new turn', this.reset)\r\n }\r\n\r\n componentWillUnmount() {\r\n this.props.solver.eventRegistry.removeEventListener('step finish', this.whenStepFinished)\r\n this.props.solver.eventRegistry.removeEventListener('new turn', this.reset)\r\n }\r\n\r\n whenStepFinished({message}: StrategyResult) {\r\n this.setState({ text: message ?? \"\" })\r\n }\r\n\r\n reset() {\r\n this.setState({ text: \"\" })\r\n }\r\n\r\n render() {\r\n return (\r\n
    \r\n Strategy explanation\r\n

    {this.state.text}

    \r\n
    \r\n )\r\n }\r\n}\r\n","\r\nimport React from 'react'\r\nimport Solver from '../../Api/Solver'\r\nimport Sudoku from '../../Api/Spaces/Sudoku'\r\nimport { StrategyResult } from '../../Api/Types'\r\nimport StrategyControls from './StrategyControls'\r\nimport StrategyList from './StrategyList'\r\nimport StrategyDetails from './StrategyDetails'\r\n\r\ntype SolverPartProps = Readonly<{\r\n sudoku: Sudoku\r\n solver: Solver\r\n}>\r\n\r\n/**\r\n * The solver part of the sudoku solver\r\n */\r\nexport default class SolverPart extends React.Component {\r\n children: {\r\n controls: null | StrategyControls\r\n list: null | StrategyList\r\n }\r\n strategyItemStates: StrategyResult[]\r\n constructor(props: SolverPartProps) {\r\n super(props)\r\n\r\n this.children = {\r\n controls: null,\r\n list: null\r\n }\r\n this.strategyItemStates = []\r\n\r\n this.whenControlsConstruct = this.whenControlsConstruct.bind(this)\r\n this.whenListConstructs = this.whenListConstructs.bind(this)\r\n }\r\n\r\n render() {\r\n return (\r\n
    \r\n \r\n
    \r\n strategies\r\n \r\n
    \r\n \r\n
    \r\n )\r\n }\r\n\r\n whenControlsConstruct(controls: StrategyControls) {\r\n this.children.controls = controls\r\n }\r\n\r\n whenListConstructs(list: StrategyList) {\r\n this.children.list = list\r\n }\r\n\r\n /** Called when a strategy starts - see the Solver api */\r\n notify(strategyIndex: number, strategyResult: StrategyResult) {\r\n this.strategyItemStates[strategyIndex] = strategyResult\r\n }\r\n}\r\n","import React from \"react\";\r\nimport { _Callback } from \"../../Types\";\r\nimport Control from \"../Control\";\r\n\r\ntype TabProps = Readonly<{\r\n index: number\r\n focused: boolean\r\n selected: boolean\r\n title: string\r\n whenFocused: _Callback\r\n whenSelected: (index: number) => void\r\n}>\r\n\r\nexport default class Tab extends React.Component {\r\n selfElement: HTMLButtonElement | null = null\r\n setSelfElement: (element: HTMLButtonElement | null) => HTMLButtonElement | null;\r\n constructor (props: TabProps) {\r\n super(props)\r\n this.setSelfElement = (element: HTMLButtonElement | null) => this.selfElement = element\r\n this.callbackIfNotSelected = this.callbackIfNotSelected.bind(this)\r\n }\r\n\r\n componentDidUpdate() {\r\n if (this.props.focused) {\r\n this.selfElement?.focus()\r\n }\r\n }\r\n\r\n render () {\r\n const className = this.props.selected ? `Tab selected` : `Tab unselected`\r\n return (\r\n {this.props.title}\r\n )\r\n }\r\n\r\n callbackIfNotSelected () {\r\n if (!this.props.selected) {\r\n this.props.whenSelected(this.props.index)\r\n }\r\n }\r\n}\r\n","import React from \"react\";\r\nimport \"./Tabs.css\"\r\n\r\nimport { _Callback } from \"../../Types\";\r\nimport Tab from \"./Tab\";\r\n\r\ntype TabsProps = Readonly<{\r\n tabNames: string[]\r\n whenTabChange: _Callback\r\n}>\r\n\r\ntype TabsState = Readonly<{\r\n focusedTab: number | null\r\n selectedTab: number\r\n}>\r\n\r\ntype TabsElement = HTMLDivElement | null\r\n\r\nconst importantKeys = new Set([\"Tab\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"] as const)\r\nconst oppositeKeys = {\r\n ArrowLeft: \"ArrowRight\",\r\n ArrowRight: \"ArrowLeft\",\r\n Home: \"End\",\r\n End: \"Home\",\r\n} as const\r\n\r\nexport default class Tabs extends React.Component {\r\n tabsElement: TabsElement = null;\r\n keysPressed: typeof importantKeys = new Set();\r\n setTabsElement: (element: TabsElement) => TabsElement;\r\n constructor (props: TabsProps) {\r\n super(props)\r\n this.state = {\r\n focusedTab: 0,\r\n selectedTab: 0,\r\n }\r\n\r\n this.setTabsElement = (element: TabsElement) => this.tabsElement = element\r\n\r\n this.whenBlur = this.whenBlur.bind(this)\r\n this.whenFocus = this.whenFocus.bind(this)\r\n this.whenKeyUp = this.whenKeyUp.bind(this)\r\n this.whenKeyDown = this.whenKeyDown.bind(this)\r\n this.whenTabChange = this.whenTabChange.bind(this)\r\n this.whenTabFocused = this.whenTabFocused.bind(this)\r\n }\r\n\r\n componentDidMount () {\r\n // @ts-expect-error Why does React have special dom types???\r\n document.addEventListener('keyup', this.whenKeyUp)\r\n }\r\n\r\n componentWillUnmount () {\r\n // @ts-expect-error Why does React have special dom types???\r\n document.removeEventListener('keyup', this.whenKeyUp)\r\n }\r\n\r\n render () {\r\n const tabs = []\r\n for (const [index, title] of this.props.tabNames.entries()) {\r\n tabs.push(\r\n \r\n )\r\n }\r\n\r\n /**\r\n * https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-1/tabs.html\r\n *\r\n * Using div here because it's recommended\r\n * tabindex for focusability but not tabbability (-1)\r\n */\r\n return (\r\n {tabs}\r\n )\r\n }\r\n\r\n whenTabChange (index: number) {\r\n this.setState({selectedTab: index})\r\n this.props.whenTabChange(index)\r\n }\r\n\r\n /** Keyboard support */\r\n\r\n whenKeyDown (event: React.KeyboardEvent) {\r\n if (importantKeys.has(event.key)) { // @ts-expect-error Can't narrow\r\n this.keysPressed.add(event.key)\r\n }\r\n\r\n const movements = new Set()\r\n let tab = false\r\n for (const key of this.keysPressed) {\r\n if (key === 'Tab') {\r\n tab = true\r\n } else if (movements.has(oppositeKeys[key])) {\r\n movements.delete(oppositeKeys[key])\r\n } else {\r\n movements.add(key)\r\n }\r\n }\r\n\r\n if (movements.size !== 0) {\r\n for (const key of ['Home', 'End', 'ArrowLeft', 'ArrowRight'] as const) {\r\n if (movements.has(key)) {\r\n this.handleMovement(key, tab)\r\n break\r\n }\r\n }\r\n } else if (tab) {\r\n this.changeToMainContent()\r\n }\r\n }\r\n\r\n private handleMovement (movement: keyof typeof oppositeKeys, tab: boolean) {\r\n let tabChanged = false\r\n this.setState((state, props) => {\r\n let newTab: number\r\n\r\n if (movement === 'End')\r\n newTab = props.tabNames.length - 1\r\n else if (movement === 'Home' || state.focusedTab === null)\r\n newTab = 0\r\n else if (movement === 'ArrowLeft')\r\n newTab = state.focusedTab === 0\r\n ? props.tabNames.length - 1\r\n : state.focusedTab - 1\r\n else if (movement === 'ArrowRight')\r\n newTab = state.focusedTab === props.tabNames.length - 1\r\n ? 0\r\n : state.focusedTab + 1\r\n else\r\n throw TypeError(`Impossible movement: ${movement}`)\r\n\r\n if (state.selectedTab === newTab) {\r\n tabChanged = true\r\n return { focusedTab: newTab, selectedTab: newTab }\r\n }\r\n\r\n return null\r\n }, () => {\r\n if (tabChanged) {\r\n this.props.whenTabChange(this.state.selectedTab)\r\n }\r\n if (tab) {\r\n this.changeToMainContent()\r\n }\r\n })\r\n }\r\n\r\n whenKeyUp (event: React.KeyboardEvent) {\r\n this.keysPressed.delete(event.key)\r\n }\r\n\r\n whenTabFocused () {\r\n // align focused and selected tab\r\n this.setState(prevState => ({ focusedTab: prevState.selectedTab }))\r\n }\r\n\r\n // When entering a group of tabs, focus the currently active (\"selected\") tab\r\n // Note that a mouseup happens after a focus event, i.e. this always wins a race to be overridden.\r\n whenFocus (_event: React.FocusEvent) {\r\n this.whenTabFocused()\r\n }\r\n\r\n whenBlur() {\r\n this.setState({ focusedTab: null })\r\n }\r\n\r\n changeToMainContent () {\r\n document.getElementById('TabContent')?.focus()\r\n }\r\n}\r\n","/**\r\n * IMPORTANT: This is represented by a section element\r\n * since it's actually directly related to the content\r\n *\r\n * But it's still... _aside_ ```
    ```\r\n */\r\n\r\nimport './Aside.css'\r\nimport React from 'react'\r\nimport SolverPart from './AsideElems/SolverPart'\r\nimport SudokuData from '../Api/Spaces/Sudoku'\r\nimport Tabs from './AsideElems/Tabs'\r\nimport Control from './Control'\r\nimport Solver from '../Api/Solver'\r\n\r\ntype AsideProps = Readonly<{\r\n sudoku: SudokuData,\r\n solver: Solver,\r\n}>\r\n\r\ntype AsideState = Readonly<{\r\n selectedTab: number\r\n}>\r\n\r\n\r\nconst tabNames = [\"solving tools\", \"strategies\"]\r\n\r\n/**\r\n * Currently a window of tabs\r\n */\r\nexport default class Aside extends React.Component {\r\n constructor (props: AsideProps) {\r\n super(props)\r\n\r\n this.state = {\r\n selectedTab: 0\r\n }\r\n\r\n this.whenTabChange = this.whenTabChange.bind(this)\r\n }\r\n\r\n render() {\r\n let content: JSX.Element\r\n if (this.state.selectedTab === 0) {\r\n content = <>\r\n \r\n \r\n \r\n \r\n } else if (this.state.selectedTab === 1) {\r\n content =\r\n \r\n } else {\r\n throw new ReferenceError(`unknown Selected tab index: ${this.state.selectedTab}`)\r\n }\r\n\r\n /**\r\n * Tabpanel id used in Tabs (aria-owns)\r\n * div because that's what's recommended\r\n * tabindex for focusability but not tabbability (-1)\r\n */\r\n return (\r\n
    \r\n \r\n
    {content}
    \r\n
    \r\n );\r\n }\r\n\r\n whenTabChange(index: number) {\r\n this.setState({ selectedTab: index })\r\n }\r\n}\r\n","import { ALL_CANDIDATES, GrpTyp, IndexToNine, INDICES_TO_NINE, SudokuDigits, ThreeDimensionalArray } from \"../../Types\"\r\nimport { boxAt, CellID, id, to9by9 } from \"../Utils\"\r\n\r\nexport type CandidateLocations = Record[]>\r\n\r\nfunction Cell (id: CellID, cell: SudokuDigits[]) {\r\n return {\r\n candidates: cell,\r\n position: id\r\n }\r\n}\r\n\r\n/**\r\n * Defines base sudoku methods\r\n * Should I move these to utils?\r\n */\r\nexport default class PureSudoku {\r\n data: ThreeDimensionalArray\r\n constructor(representation?: string) {\r\n this.data = [\r\n [], [], [],\r\n [], [], [],\r\n [], [], [],\r\n ]\r\n\r\n for (let i = 0; i < 9; i++) {\r\n for (let j = 0; j < 9; j++) {\r\n this.data[i][j] = [1, 2, 3, 4, 5, 6, 7, 8, 9]\r\n }\r\n }\r\n\r\n if (typeof representation === \"string\") {\r\n this.import(representation)\r\n }\r\n }\r\n\r\n static fromRepresentation(this: T, representation: string): InstanceType {\r\n return new this(representation) as InstanceType\r\n }\r\n\r\n /**\r\n * Convert the sudoku into 81 digits\r\n * \"0\" = a cell with no candidates\r\n * \".\" = an unsolved cell\r\n */\r\n to81 () {\r\n let str = \"\"\r\n for (const row of this.data) {\r\n for (const cell of row) {\r\n if (cell.length === 1) {\r\n str += cell[0]\r\n } else if (cell.length === 0) {\r\n str += \"0\"\r\n } else {\r\n str += \".\"\r\n }\r\n }\r\n str += \"\\n\"\r\n }\r\n return str\r\n }\r\n\r\n /**\r\n * Same as to81 but every cell is represented by\r\n * its candidates. No newlines.\r\n * Missing candidates are represented by \".\"\r\n */\r\n to729 () {\r\n let str = \"\"\r\n for (const row of this.data) {\r\n for (const cell of row) {\r\n for (const candidate of ALL_CANDIDATES) {\r\n str += cell.includes(candidate) ? candidate : \".\"\r\n }\r\n }\r\n }\r\n return str\r\n }\r\n\r\n /**\r\n * Imports from an 81 character string representing a sudoku.\r\n *\r\n * Any character that is not a digit is a blank cell.\r\n */\r\n import81(representation: string): void {\r\n let totalIndex = 0\r\n for (const i of INDICES_TO_NINE) {\r\n for (const j of INDICES_TO_NINE) {\r\n const char = representation[totalIndex]\r\n totalIndex++ // after char\r\n\r\n // Using `this.set` for compatibility with `Sudoku`\r\n if (\"123456789\".includes(char)) {\r\n this.set(i, j).to(Number(char) as SudokuDigits)\r\n } else {\r\n this.set(i, j).to(1, 2, 3, 4, 5, 6, 7, 8, 9)\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Imports from a 729 character string, where every 9 candidates\r\n * specify candidates for a cell\r\n */\r\n import729(representation: string): void {\r\n let totalIndex = 0\r\n for (const i of INDICES_TO_NINE) {\r\n for (const j of INDICES_TO_NINE) {\r\n const candidateData = (\r\n representation.slice(totalIndex * 9, totalIndex * 9 + 9)\r\n .split('')\r\n .filter(candidate => \"123456789\".includes(candidate))\r\n .map(candidate => Number(candidate) as SudokuDigits)\r\n )\r\n\r\n totalIndex++\r\n this.set(i, j).to(...candidateData)\r\n }\r\n }\r\n }\r\n\r\n importGrid(gridRepresentation: ThreeDimensionalArray) {\r\n for (const i of INDICES_TO_NINE) {\r\n for (const j of INDICES_TO_NINE) {\r\n this.set(i, j).to(...gridRepresentation[i][j])\r\n }\r\n }\r\n }\r\n\r\n // Note: import is a keyword, but it doesn't cause a syntax error here\r\n import(representation: string) {\r\n representation = representation.trim().normalize()\r\n const representationWithoutWhitespace = representation.replaceAll(/\\s/g, \"\")\r\n const onlyDigitRepresentation = representation.replaceAll(/\\D/g, \"\")\r\n const oneToNineOrDot = representation.replaceAll(/[^.1-9]/g, \"\")\r\n\r\n for (const testRepresentation of [representation, representationWithoutWhitespace, onlyDigitRepresentation, oneToNineOrDot] as const) {\r\n if (testRepresentation.length === 81) {\r\n this.import81(testRepresentation)\r\n return {\r\n success: true,\r\n representationType: '81'\r\n } as const\r\n }\r\n\r\n if (testRepresentation.length === 729) {\r\n this.import729(testRepresentation)\r\n return {\r\n success: true,\r\n representationType: '729'\r\n } as const\r\n }\r\n }\r\n\r\n\r\n const gridRepresentation = representation\r\n .split('')\r\n .filter(char => \"123456789 \".includes(char))\r\n .join('')\r\n .trim()\r\n .split(/\\s+/) // split ignores g flag\r\n\r\n if (gridRepresentation.length === 81) {\r\n this.importGrid(\r\n to9by9(\r\n gridRepresentation.map(\r\n a => a.split('').map(b => Number(b) as SudokuDigits)\r\n )\r\n )\r\n )\r\n\r\n return {\r\n success: true,\r\n representationType: 'grid'\r\n } as const\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n }\r\n\r\n // WARNING: The \"candidates\" aren't checked (other than the static type checking)\r\n set(x: IndexToNine, y: IndexToNine) {\r\n return {\r\n to: (...candidates: SudokuDigits[]) => {\r\n this.data[x][y] = candidates\r\n }\r\n }\r\n }\r\n\r\n clearCell(x: IndexToNine, y: IndexToNine) {\r\n this.set(x, y).to(1, 2, 3, 4, 5, 6, 7, 8, 9)\r\n }\r\n\r\n clear() {\r\n for (const i of INDICES_TO_NINE) {\r\n for (const j of INDICES_TO_NINE) {\r\n this.clearCell(i, j)\r\n }\r\n }\r\n }\r\n\r\n getColumn(index: IndexToNine) {\r\n return this.data.map(row => row[index])\r\n }\r\n\r\n getBox(index: IndexToNine) {\r\n // 0 1 2 -> 0 3 6\r\n const startRow = index - (index % 3) // / 3 * 3\r\n const startColumn = (index % 3) * 3\r\n return this.data.slice(startRow, startRow + 3).flatMap(row => row.slice(startColumn, startColumn + 3))\r\n }\r\n\r\n getBoxGroup(index: IndexToNine, data: T[][]) {\r\n const startRow = index - (index % 3)\r\n const startColumn = (index % 3) * 3\r\n return data.slice(startRow, startRow + 3).flatMap(row => row.slice(startColumn, startColumn + 3))\r\n }\r\n\r\n /**\r\n * A group is a set of maximally mutually exclusive set of cells.\r\n * \r\n * Currently, we assume this is just the rows, columns, and boxes, but this will be changed later on.\r\n */\r\n getGroups() {\r\n const groups = []\r\n const cellData = this.data.map((row, indexOfRow) =>\r\n row.map((cell, indexInRow) => Cell(id(indexOfRow as IndexToNine, indexInRow as IndexToNine), cell))\r\n )\r\n\r\n for (const i of INDICES_TO_NINE) {\r\n groups.push(\r\n cellData[i],\r\n cellData.map(row => row[i]),\r\n this.getBoxGroup(i, cellData)\r\n )\r\n }\r\n\r\n return groups\r\n }\r\n\r\n /**\r\n * Returns the candidate locations of the sudoku\r\n *\r\n * @misnomer\r\n * getCandidatesLocations\r\n *\r\n * @example\r\n *\r\n * ```ts\r\n * sudoku.getCandidatesLocations()[candidate].rows[5]\r\n * // > Set\r\n * // set of cells with that candidate\r\n * ```\r\n *\r\n * Return type is an array of\r\n * ```\r\n * CandidateLocations: {\r\n * rows: [Set for each index]\r\n * columns: [Set for each index]\r\n * boxes: [Set for each index]\r\n * }\r\n * ```\r\n */\r\n getCandidateLocations(): CandidateLocations[] {\r\n const candidateLocations = [] as CandidateLocations[]\r\n\r\n for (const candidate of ALL_CANDIDATES) {\r\n candidateLocations[candidate] = {\r\n row: [new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set()],\r\n column: [new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set()],\r\n box: [new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set(), new Set()]\r\n }\r\n }\r\n\r\n for (const row of INDICES_TO_NINE) {\r\n for (const column of INDICES_TO_NINE) {\r\n const cellID = id(row, column)\r\n for (const candidate of this.data[row][column]) {\r\n candidateLocations[candidate].row[row].add(cellID)\r\n candidateLocations[candidate].column[column].add(cellID)\r\n candidateLocations[candidate].box[boxAt(row, column)].add(cellID)\r\n }\r\n }\r\n }\r\n\r\n return candidateLocations\r\n }\r\n\r\n /**\r\n * Removes a candidate at a cell\r\n *\r\n * @example\r\n * (new PureSudoku()).remove(7).at(3, 5)\r\n */\r\n remove(candidate: SudokuDigits) {\r\n // Using an arrow function here to use `this`\r\n return {\r\n at: (row: IndexToNine, column: IndexToNine) => {\r\n this.set(row, column).to(\r\n ...this.data[row][column].filter(candidatee => candidatee !== candidate)\r\n )\r\n }\r\n }\r\n }\r\n}\r\n\r\n","import Cell from \"../../Elems/MainElems/Cell\"\r\nimport { IndexToNine, SudokuDigits, TwoDimensionalArray } from \"../../Types\"\r\nimport PureSudoku from \"./PureSudoku\"\r\n\r\n/**\r\n * Wraps PureSudoku, to sync the data with the Cell components.\r\n * \r\n * It is also used by the Sudoku component to indirectly access the Cell components. -.-\r\n * \r\n * For sanity, this class should be kept very simple.\r\n */\r\nexport default class Sudoku extends PureSudoku {\r\n cells: TwoDimensionalArray\r\n constructor (...args: ConstructorParameters) {\r\n super(...args)\r\n if (this.data === undefined) {\r\n throw TypeError('Super constructor PureSudoku didnt initialize this.data')\r\n }\r\n\r\n this.cells = [\r\n [], [], [],\r\n [], [], [],\r\n [], [], [],\r\n ]\r\n }\r\n\r\n override set(x: IndexToNine, y: IndexToNine) {\r\n return {\r\n to: async (...candidates: SudokuDigits[]) => {\r\n this.data[x][y] = candidates\r\n\r\n // super calls set() before defining this.cells\r\n if (this.cells !== undefined) {\r\n await new Promise(resolve => {\r\n this.cells[x][y]?.setCandidatesTo(candidates, () => resolve(undefined))\r\n })\r\n }\r\n }\r\n }\r\n }\r\n\r\n override clearCell(x: IndexToNine, y: IndexToNine) {\r\n this.data[x][y] = [1, 2, 3, 4, 5, 6, 7, 8, 9]\r\n this.cells[x][y]?.clearCandidates()\r\n }\r\n\r\n /**\r\n * Used for Cell initialization\r\n *\r\n * An alternate possibility is having the cell reflect the data, but\r\n * that allows inconsistencies between the visuals and the data.\r\n */\r\n addCell(cell: Cell) {\r\n this.cells[cell.props.row][cell.props.column] = cell\r\n this.data[cell.props.row][cell.props.column] = cell.state.candidates\r\n }\r\n\r\n removeCell(cell: Cell) {\r\n this.cells[cell.props.row][cell.props.column] = undefined\r\n // this.data[cell.props.row][cell.props.column] = cell.state.candidates\r\n }\r\n}\r\n","/**\r\n * Same as Utils, but ones dependent on other files.\r\n *\r\n * Might want to run `madge --circular --extensions ts ./src` after importing from here\r\n */\r\n\r\nimport { SudokuDigits, INDICES_TO_NINE, IndexToNine } from \"../Types\";\r\nimport PureSudoku from \"./Spaces/PureSudoku\";\r\nimport Sudoku from \"./Spaces/Sudoku\";\r\nimport { CandidateID, CellID, id } from \"./Utils\";\r\n\r\n/**\r\n * Highlights a cell, see {@link Cell#highlight}\r\n * Default background is blue.\r\n */\r\nexport function highlightCell (sudoku: PureSudoku, {row, column}: CellID, color = 'blue') {\r\n if (sudoku instanceof Sudoku) {\r\n sudoku.cells[row][column]?.addClass(color)\r\n }\r\n}\r\n\r\n/**\r\n * Highlights multiple cells, see {@link Cell#highlight}\r\n * Default background is blue.\r\n */\r\nexport function highlightGroup (sudoku: PureSudoku, group: Iterable, color = 'blue') {\r\n if (sudoku instanceof Sudoku) {\r\n for (const {row, column} of group) {\r\n sudoku.cells[row][column]?.addClass(color)\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Colors a group of cells' candidates, see {@link Cell#highlight}\r\n *\r\n * The precedence for colors is:\r\n * 1. orange\r\n * 2. green\r\n * 3. blue\r\n */\r\nexport function colorGroup (sudoku: PureSudoku, group: Iterable, candidate: SudokuDigits, color = 'blue') {\r\n if (sudoku instanceof Sudoku) {\r\n for (const cell of group) {\r\n const element = sudoku.cells[cell.row][cell.column];\r\n element?.highlight([candidate], color);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Same as {@link colorGroup}, but this time with a specific candidate\r\n */\r\nexport function colorCandidateF (sudoku: PureSudoku, row: IndexToNine, column: IndexToNine, digit: SudokuDigits, color = 'blue') {\r\n if (sudoku instanceof Sudoku) {\r\n sudoku.cells[row][column]?.highlight([digit], color)\r\n }\r\n}\r\n\r\n/**\r\n * Same as {@link colorGroup}, but this time with a specific candidate\r\n */\r\nexport function colorCandidate (sudoku: PureSudoku, { row, column, digit }: CandidateID, color = 'blue') {\r\n colorCandidateF(sudoku, row, column, digit, color)\r\n}\r\n\r\nexport function numberOfCellsWithNCandidates (sudoku: PureSudoku, N: number) {\r\n let cellsWithNCandidates = 0\r\n for (const row of INDICES_TO_NINE) {\r\n for (const column of INDICES_TO_NINE) {\r\n if (sudoku.data[row][column].length === N) {\r\n cellsWithNCandidates++\r\n }\r\n }\r\n }\r\n\r\n return cellsWithNCandidates\r\n}\r\n\r\nexport function getCellsWithNCandidates (sudoku: PureSudoku, N: number) {\r\n const cellsWithNCandidates = [] as CellID[]\r\n for (const row of INDICES_TO_NINE) {\r\n for (const column of INDICES_TO_NINE) {\r\n if (sudoku.data[row][column].length === N) {\r\n cellsWithNCandidates.push(id(row, column))\r\n }\r\n }\r\n }\r\n\r\n return cellsWithNCandidates\r\n}\r\n\r\n/**\r\n * Removes a {@param candidate} from multiple {@param cells}\r\n * if that candidate exists.\r\n *\r\n * Returns true if any candidates were eliminated\r\n */\r\nexport function removeCandidateFromCells(sudoku: PureSudoku, candidate: SudokuDigits, cells: Iterable) {\r\n // Should this return early if there are no cells?\r\n let success = false\r\n for (const {row, column} of cells) {\r\n if (sudoku.data[row][column].includes(candidate)) {\r\n sudoku.remove(candidate).at(row, column)\r\n success = true\r\n }\r\n }\r\n return success\r\n}\r\n\r\nexport function wouldRemoveCandidateFromCells(sudoku: PureSudoku, candidate: SudokuDigits, cells: Iterable) {\r\n for (const { row, column } of cells) {\r\n if (sudoku.data[row][column].includes(candidate)) {\r\n return true\r\n }\r\n }\r\n return false\r\n}\r\n","import { SudokuDigits, ROW_NAMES, COLUMN_NAMES, ALL_CANDIDATES, BOX_NAMES, INDICES_TO_NINE } from \"../../Types\"\r\nimport { convertArrayToEnglishList } from \"../../utils\"\r\nimport PureSudoku from \"../Spaces/PureSudoku\"\r\nimport { boxAt, algebraic, boxNameAt } from \"../Utils\"\r\n\r\ntype validityResult = {\r\n ok: true\r\n} | {\r\n ok: false,\r\n message: string\r\n}\r\n\r\nfunction listOfMissingCandidatesIn (group: Set) {\r\n return convertArrayToEnglishList(\r\n ALL_CANDIDATES.filter(\r\n candidate => !group.has(candidate)\r\n )\r\n )\r\n}\r\n\r\nexport default function checkValidity(sudoku: PureSudoku): validityResult {\r\n const solvedInColumns = [] as Array>\r\n const solvedInBoxes = [] as Array>\r\n const candidatesInColumns = [] as Array>\r\n const candidatesInBoxes = [] as Array>\r\n\r\n for (let i = 0; i < 9; i++) {\r\n solvedInColumns.push(new Set())\r\n solvedInBoxes.push(new Set())\r\n candidatesInColumns.push(new Set())\r\n candidatesInBoxes.push(new Set())\r\n }\r\n\r\n for (const i of INDICES_TO_NINE) {\r\n const solvedInRow = new Set()\r\n const candidatesInRow = new Set()\r\n\r\n for (const j of INDICES_TO_NINE) {\r\n const candidates = sudoku.data[i][j]\r\n const current = {\r\n column: j,\r\n box: boxAt(i, j)\r\n } as const\r\n\r\n // No possibilities\r\n if (candidates.length === 0) {\r\n return {\r\n ok: false,\r\n message: `Cell ${algebraic(i, j)} has 0 possible candidates!`\r\n }\r\n }\r\n\r\n if (candidates.length === 1) {\r\n const solvedCandidate = candidates[0]\r\n\r\n // Same in row\r\n if (solvedInRow.has(solvedCandidate)) {\r\n return {\r\n ok: false,\r\n message: `Two (or more) ${solvedCandidate}s in row ${ROW_NAMES[i]}!`\r\n }\r\n }\r\n\r\n // Same in column\r\n if (solvedInColumns[current.column].has(solvedCandidate)) {\r\n return {\r\n ok: false,\r\n message: `Two (or more) ${solvedCandidate}s in column ${COLUMN_NAMES[j]}!`\r\n }\r\n }\r\n\r\n // Same in box\r\n if (solvedInBoxes[current.box].has(solvedCandidate)) {\r\n return {\r\n ok: false,\r\n message: `Two (or more) ${solvedCandidate}s in box ${boxNameAt(i, j)}!`\r\n }\r\n }\r\n\r\n solvedInRow.add(solvedCandidate)\r\n solvedInColumns[current.column].add(solvedCandidate)\r\n solvedInBoxes[current.box].add(solvedCandidate)\r\n }\r\n\r\n for (const candidate of candidates) {\r\n candidatesInRow.add(candidate)\r\n candidatesInColumns[current.column].add(candidate)\r\n candidatesInBoxes[current.box].add(candidate)\r\n }\r\n }\r\n\r\n if (candidatesInRow.size !== 9) {\r\n return {\r\n ok: false,\r\n message: `Row ${ROW_NAMES[i]} has 0 possibilities for ${listOfMissingCandidatesIn(candidatesInRow)}!!!`\r\n }\r\n }\r\n }\r\n\r\n for (let i = 0; i < 9; i++) {\r\n if (candidatesInColumns[i].size !== 9) {\r\n return {\r\n ok: false,\r\n message: `Column ${COLUMN_NAMES[i]} has 0 possibilities for ${listOfMissingCandidatesIn(candidatesInColumns[i])}!!!`\r\n }\r\n }\r\n\r\n if (candidatesInBoxes[i].size !== 9) {\r\n return {\r\n ok: false,\r\n message: `Box ${BOX_NAMES[i]} has 0 possibilities for ${listOfMissingCandidatesIn(candidatesInBoxes[i])}!!!`\r\n }\r\n }\r\n }\r\n\r\n return {\r\n ok: true\r\n }\r\n}\r\n","\r\n\r\nimport { AlertType, SudokuDigits } from \"../../Types\";\r\nimport { convertArrayToEnglishList } from \"../../utils\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport Sudoku from \"../Spaces/Sudoku\";\r\nimport { CellGroup, SuccessError } from \"../Types\";\r\nimport { algebraic } from \"../Utils\";\r\n\r\n/**\r\n * Gets the unique combinations of an array\\\r\n * All elements are unmodified and assumed different\r\n *\r\n * \"combinations\" is in the mathematical sense:\r\n * if you give 7 elements, with min = 2 and max = 4,\r\n * you get (7 choose 2) + (7 choose 3) + (7 choose 4) elements.\r\n *\r\n * @example\r\n * combinations([1, 2, 3])\r\n * // [[3], [3, 2], [3, 2, 1], [3, 1], [2], [2, 1], [1]]\r\n *\r\n * @param {number} min - The minimum size of a combination\r\n * @param {number} max - The maximum size of a combination\r\n */\r\nexport function combinations(array: T[], min = 1, max = array.length, currentCount = 1) {\r\n const _combinations: T[][] = []\r\n const _arrayCopy = array.slice()\r\n while (_arrayCopy.length) {\r\n const element = _arrayCopy.pop() as T\r\n\r\n // For combinations shorter than max size, but also includes max size\r\n if (currentCount >= min) {\r\n _combinations.push([element])\r\n }\r\n\r\n // After the check\r\n if (currentCount === max) {\r\n continue\r\n }\r\n\r\n for (const combination of combinations(_arrayCopy, min, max, currentCount + 1)) {\r\n _combinations.push([element, ...combination])\r\n }\r\n }\r\n\r\n return _combinations\r\n}\r\n\r\n/**\r\n * Return a set of unique candidates in a conjugate\r\n */\r\nfunction getCandidatesOfConjugate(conjugate: CellGroup) {\r\n // Array from the values of a set\r\n // The set is the accumulated candidates\r\n return conjugate.reduce(\r\n (accum, currentCell) => {\r\n for (const candidate of currentCell.candidates) {\r\n accum.add(candidate)\r\n }\r\n return accum\r\n }, new Set()\r\n )\r\n}\r\n\r\n// Inner inner function to make things look nicer below\r\nfunction __errorHandling (conjugate: CellGroup, invalidGroupCandidates: Set) {\r\n const invalidGroupNames = convertArrayToEnglishList(\r\n conjugate.map(someCell => algebraic(someCell.position.row, someCell.position.column))\r\n )\r\n const invalidCandidateString = convertArrayToEnglishList(Array.from(invalidGroupCandidates).sort())\r\n\r\n if (conjugate.length === 1) {\r\n // Never happens since cells are filtered away\r\n window._custom.alert(`The cell ${invalidGroupNames} has 0 possibilities!`, AlertType.ERROR)\r\n } else if (invalidGroupCandidates.size === 1) {\r\n // Never happens\r\n window._custom.alert(`${invalidGroupNames}: ${conjugate.length} cells cannot share 1 candidate (${invalidCandidateString})!!!`, AlertType.ERROR)\r\n } else {\r\n window._custom.alert(`${invalidGroupNames}: ${conjugate.length} cells cannot share ${invalidGroupCandidates.size} candidates (${invalidCandidateString})!!!`, AlertType.ERROR)\r\n }\r\n}\r\n\r\n/**\r\n * Here, a \"group\" is a row, column, or box, but can be any group.\r\n *\r\n * Within that group, we're trying to find subgroups, aka conjugates\r\n * where such subgroup has n cells and n candidates.\r\n *\r\n * In Andrew Stuart's solver, this is equivalent to finding\r\n * naked pairs, triples, and quads.\r\n *\r\n * @param group - A group of cells. Generally a row, column, or box\r\n * @param maxSize - The maximum size of the conjugate. Default is 4.\r\n * (Not looking for conjugates of size 5 or more, since then there would be a\r\n * size 4 with the other cells by default. TODO better explanation)\r\n */\r\nfunction findConjugatesOfGroup(group: CellGroup, maxSize = 4 as 2 | 3 | 4) {\r\n // 1. Filter the possible cells\r\n // Each possible cell must have from 2 to maxSize candidates\r\n const possibleCells = group.filter(cell => 1 < cell.candidates.length && cell.candidates.length <= maxSize)\r\n\r\n // 2. Now that the cells are filtered actually find the conjugates\r\n const conjugates = [] as CellGroup[]\r\n for (const conjugate of combinations(possibleCells, 2, maxSize)) {\r\n const candidatesOfConjugate = getCandidatesOfConjugate(conjugate)\r\n\r\n // For example 3 cells needing 2 candidates = invalid.\r\n if (conjugate.length > candidatesOfConjugate.size) {\r\n __errorHandling(conjugate, candidatesOfConjugate)\r\n return \"ERROR!!!\" as const\r\n } else if (conjugate.length === candidatesOfConjugate.size) {\r\n // Found a conjugate!!!!!\r\n conjugates.push(conjugate)\r\n }\r\n }\r\n\r\n return conjugates\r\n}\r\n\r\n\r\n// Idea for hidden:\r\n// For each candidate find squares\r\n\r\n/**\r\n * Colors a conjugate, see Cell#highlight\r\n */\r\nexport function colorConjugate(sudoku: PureSudoku, conjugate: CellGroup, color = 'blue') {\r\n if (sudoku instanceof Sudoku) {\r\n for (const cell of conjugate) {\r\n const element = sudoku.cells[cell.position.row][cell.position.column]\r\n element?.highlight(cell.candidates, color)\r\n }\r\n }\r\n}\r\n\r\nfunction eliminateUsingConjugate(\r\n sudoku: PureSudoku,\r\n group: CellGroup,\r\n conjugate: CellGroup,\r\n) {\r\n let successcount = 0\r\n const conjugateCandidates = getCandidatesOfConjugate(conjugate)\r\n const eliminatedFrom = []\r\n\r\n for (const cell of group) {\r\n // If this cell is not in the conjugate\r\n if (!conjugate.some(jCell => jCell.position === cell.position)) {\r\n\r\n // The cell now cannot have any of the candidates in the conjugate!!!\r\n const nonConjugateCandidates = cell.candidates.filter(candidate => !conjugateCandidates.has(candidate))\r\n if (cell.candidates.length !== nonConjugateCandidates.length) { // If has any...\r\n successcount++ // Success!\r\n colorConjugate(sudoku, conjugate)\r\n sudoku.set(cell.position.row, cell.position.column).to(...nonConjugateCandidates)\r\n eliminatedFrom.push(algebraic(cell.position.row, cell.position.column))\r\n }\r\n }\r\n }\r\n\r\n return [\r\n successcount,\r\n `${[...conjugateCandidates].join(\"\")} ${conjugate.map(cell => algebraic(cell.position.row, cell.position.column))} ⇒ ${eliminatedFrom}`,\r\n ] as const\r\n}\r\n\r\nfunction eliminateUsingConjugates(sudoku: PureSudoku, groups: CellGroup[], conjugatesOfGroup: CellGroup[][]) {\r\n let successcount = 0\r\n const messages = []\r\n for (const [i, group] of groups.entries()) {\r\n for (const conjugate of conjugatesOfGroup[i]) {\r\n const [successes, message] = eliminateUsingConjugate(sudoku, group, conjugate)\r\n successcount += successes\r\n if (successes) {\r\n messages.push(message)\r\n }\r\n }\r\n }\r\n\r\n return [successcount, messages.join(\"\\n\")] as const\r\n}\r\n\r\n// Math.max(O(n^5), O(n^5))\r\nexport default function pairsTriplesAndQuads(sudoku: PureSudoku) {\r\n const groups = sudoku.getGroups()\r\n const conjugatesOfGroup = []\r\n for (const group of groups) {\r\n const conjugate = findConjugatesOfGroup(group)\r\n if (conjugate === \"ERROR!!!\") {\r\n return {\r\n success: false,\r\n successcount: SuccessError\r\n } as const\r\n }\r\n conjugatesOfGroup.push(conjugate)\r\n }\r\n\r\n const [successcount, message] = eliminateUsingConjugates(sudoku, groups, conjugatesOfGroup)\r\n\r\n if (successcount === 0) {\r\n return {\r\n success: false\r\n } as const\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount,\r\n message,\r\n } as const\r\n}\r\n","import { AlertType, BOX_NAMES, COLUMN_NAMES, INDICES_TO_NINE, ROW_NAMES, SudokuDigits } from \"../../Types\";\r\nimport { convertArrayToEnglishList } from \"../../utils\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { CellGroup, CellInfo, SuccessError } from \"../Types\";\r\nimport { algebraic, removeFromArray } from \"../Utils\";\r\nimport { colorConjugate, combinations } from \"./pairsTriplesAndQuads\";\r\n\r\n/**\r\n * Returns an array of all the cells which contain at least one of the candidates\r\n */\r\nfunction getConjugateFromCandidates (cells: CellGroup, candidates: SudokuDigits[]) {\r\n return cells.filter(cell =>\r\n candidates.some(candidate => cell.candidates.includes(candidate))\r\n )\r\n}\r\n\r\nfunction __errorHandling (candidatesOfConjugate: SudokuDigits[], conjugate: CellInfo[]) {\r\n const invalidCandidateString = convertArrayToEnglishList(candidatesOfConjugate)\r\n\r\n // To prevent errors in convertArrayToEnglishList\r\n if (conjugate.length === 0) {\r\n // A previous elimination must've caused this!\r\n return ` has 0 possibilities for ${invalidCandidateString}!!!\\n`\r\n }\r\n const invalidGroupNames = convertArrayToEnglishList(\r\n conjugate.map(someCell => algebraic(someCell.position.row, someCell.position.column))\r\n )\r\n\r\n if (candidatesOfConjugate.length === 1) {\r\n return ` has 0 possibilities for ${invalidCandidateString}!!!`\r\n } else if (conjugate.length === 1) {\r\n return `: ${candidatesOfConjugate.length} candidates (${invalidCandidateString}) all want to be in ${invalidGroupNames} which is impossible!!!`\r\n } else {\r\n return `: ${candidatesOfConjugate.length} candidates (${invalidCandidateString}) all want to be in ${conjugate.length} cells (${invalidGroupNames}) which is impossible!!!`\r\n }\r\n}\r\n\r\nfunction __filterPossibleCandidates (group: CellGroup, maxSize: number, possibleCandidates: Set) {\r\n\r\n function removeCandidate (candidate: SudokuDigits) {\r\n possibleCandidates.delete(candidate)\r\n\r\n for (const cell of group) {\r\n removeFromArray(candidate, cell.candidates)\r\n }\r\n }\r\n\r\n // a. Remove candidates that occur > maxSize times\r\n const occurances = [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0] as const as Record\r\n\r\n for (const cell of group) {\r\n for (const candidate of cell.candidates) {\r\n occurances[candidate]++\r\n }\r\n }\r\n\r\n for (const candidate of possibleCandidates) {\r\n if (occurances[candidate] > maxSize) {\r\n removeCandidate(candidate)\r\n } else if (occurances[candidate] === 0) {\r\n return `There is nowhere to put ${candidate}!` as const\r\n }\r\n }\r\n\r\n // b. Remove candidates that are alone in a cell\r\n // maxSize is now possibleCandidates.size\r\n let keepGoing = true\r\n while (keepGoing) {\r\n keepGoing = false\r\n for (const cell of group) {\r\n if (cell.candidates.length === 1) {\r\n removeCandidate(cell.candidates[0])\r\n keepGoing = true\r\n }\r\n }\r\n\r\n for (const candidate of possibleCandidates) {\r\n if (occurances[candidate] > possibleCandidates.size) {\r\n removeCandidate(candidate)\r\n keepGoing = true\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Here, a \"group\" is a row, column, or box, but can be any group.\r\n *\r\n * Within that group, we're trying to find\r\n * n candidates which can only go in n cells.\r\n *\r\n * In Andrew Stuart's solver, this is equivalent to finding\r\n * hidden pairs, triples, and quads.\r\n *\r\n * See {@link hiddenPairsTriplesAndQuads}\r\n *\r\n * I think this is O(n^4)\r\n *\r\n * @param group - A group of cells. Generally a row, column, or box.\r\n * @param maxSize - The maximum size of the conjugate. Default is 4.\r\n * (Not looking for conjugates of size 5 or more, since then there would be a\r\n * size 4 with the other cells by default. TODO better explanation)\r\n */\r\nfunction findHiddenConjugatesOfGroup(group: CellGroup, maxSize = 4 as 2 | 3 | 4) {\r\n // copy cell objects and arrays before changing them\r\n group = group.map(cell => ({\r\n position: cell.position,\r\n candidates: cell.candidates.slice()\r\n }))\r\n\r\n // 1. Filter the possible candidates (return if error)\r\n const possibleCandidates = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9] as const)\r\n const __result = __filterPossibleCandidates(group, maxSize, possibleCandidates)\r\n if (typeof __result === \"string\") {\r\n return __result\r\n }\r\n\r\n // c. Filter out cells that have too few candidates\r\n // (No limit on max candidates)\r\n const possibleCells = group.filter(cell => 1 < cell.candidates.length)\r\n\r\n if (possibleCandidates.size < 2 || possibleCells.length < 2) {\r\n return []\r\n }\r\n\r\n // 2. Do the regular pairsTriplesAndQuads function.\r\n // Time to find the conjugates, this time n candidates that must be in n cells\r\n\r\n maxSize = Math.min(maxSize, possibleCells.length, possibleCandidates.size) as 2 | 3 | 4\r\n\r\n const conjugates = []\r\n const conjugateCands = [] // Only used in one location\r\n\r\n for (const candidatesOfConjugate of combinations(Array.from(possibleCandidates), 2, maxSize)) {\r\n const conjugate = getConjugateFromCandidates(possibleCells, candidatesOfConjugate)\r\n\r\n // if (candidatesOfConjugate.some(candidate => conjugate.every(cell => !cell.candidates.includes(candidate)))) {\r\n // throw new TypeError(JSON.stringify([group, conjugate, candidatesOfConjugate]))\r\n // }\r\n\r\n // e.g.: 3 candidates must be in 2 cells\r\n if (candidatesOfConjugate.length > conjugate.length) {\r\n return __errorHandling(candidatesOfConjugate, conjugate)\r\n } else if (candidatesOfConjugate.length === conjugate.length) {\r\n // A conjugate was found!!\r\n\r\n // Check if this conjugate exactly overlaps a previous one\r\n // If so, error just like above\r\n for (const [i, prevConjugate] of conjugates.entries()) {\r\n if (prevConjugate.length === conjugate.length && prevConjugate.every(cell => conjugate.some(cell2 => cell.position === cell2.position))) {\r\n return __errorHandling([...new Set(candidatesOfConjugate.concat(conjugateCands[i]))], conjugate)\r\n }\r\n }\r\n\r\n // Filter extra candidates\r\n const filteredConjugate = conjugate.map(cell => ({\r\n candidates: cell.candidates.filter(\r\n candidate => candidatesOfConjugate.includes(candidate)\r\n ),\r\n position: cell.position,\r\n }))\r\n\r\n conjugates.push(filteredConjugate)\r\n conjugateCands.push(candidatesOfConjugate)\r\n }\r\n }\r\n\r\n return conjugates\r\n}\r\n\r\n\r\nfunction findHiddenConjugatesOfSudoku(sudoku: PureSudoku, maxSize = 4 as 2 | 3 | 4) {\r\n const conjugates = [] as CellGroup[]\r\n const groups = sudoku.getGroups()\r\n for (const i of INDICES_TO_NINE) {\r\n const resultRow = findHiddenConjugatesOfGroup(groups[i * 3], maxSize)\r\n if (typeof resultRow === \"string\") {\r\n return `Row ${ROW_NAMES[i]}${resultRow}`\r\n }\r\n\r\n const resultColumn = findHiddenConjugatesOfGroup(groups[i * 3 + 1], maxSize)\r\n if (typeof resultColumn === \"string\") {\r\n return `Column ${COLUMN_NAMES[i]}${resultColumn}`\r\n }\r\n\r\n const resultBox = findHiddenConjugatesOfGroup(groups[i * 3 + 2], maxSize)\r\n if (typeof resultBox === \"string\") {\r\n return `Box ${BOX_NAMES[i]}${resultBox}`\r\n }\r\n\r\n conjugates.push(...resultRow, ...resultColumn, ...resultBox)\r\n }\r\n\r\n return conjugates\r\n}\r\n\r\n/**\r\n * You should probably look at {@link findHiddenConjugatesOfGroup}\r\n *\r\n * Consider the following:\r\n *\r\n * ```\r\n * ...45..89 .234567.9 ..34567.9\r\n * ....5..8. .23.5.... ..3.5....\r\n * 1........ .23.5...9 ..3.5...9\r\n * ```\r\n *\r\n * The hidden pair is `67`... but how is this detected?\r\n *\r\n * ## How to find hidden conjugates programmatically\r\n *\r\n * 1. First remove candidates that occur > 4 times\\\r\n * (`3` `5` and `9` are removed)\r\n *\r\n * ```\r\n * ...4...8. .2.4.67.. ...4.67..\r\n * .......8. .2....... .........\r\n * 1........ .2....... .........\r\n * ```\r\n *\r\n * 2. Remove candidates that are alone in a cell\\\r\n * (`2` `4` and `8` are removed)\r\n *\r\n * ```\r\n * ......... .....67.. .....67..\r\n * ......... ......... .........\r\n * ......... ......... .........\r\n * ```\r\n *\r\n * 3. Tada!!!!! Found them! (Use naked conjugate function)\r\n *\r\n * ### Footnote about step 2:\r\n *\r\n * If that lone candidate *was* part of a hidden pair/triple/quad\r\n * `123...... 123...... ..3......`\r\n *\r\n * then there would be a simpler hidden pair/triple/quad:\r\n * `12....... 12....... .........`\r\n */\r\nexport default function hiddenPairsTriplesAndQuads(sudoku: PureSudoku) {\r\n let successcount = 0\r\n\r\n const result = findHiddenConjugatesOfSudoku(sudoku)\r\n if (typeof result === \"string\") {\r\n window._custom.alert(result, AlertType.ERROR)\r\n return {\r\n success: false,\r\n successcount: SuccessError\r\n } as const\r\n }\r\n\r\n for (const conjugate of result) {\r\n let success = false\r\n\r\n for (const conjugateCell of conjugate) {\r\n const actualCell = sudoku.data[conjugateCell.position.row][conjugateCell.position.column]\r\n\r\n // If different, replace\r\n // Note: Candidates are only ever removed so just check lengths\r\n if (actualCell.length > conjugateCell.candidates.length) {\r\n sudoku.set(conjugateCell.position.row, conjugateCell.position.column).to(...conjugateCell.candidates)\r\n colorConjugate(sudoku, conjugate, 'solved')\r\n success = true\r\n }\r\n }\r\n\r\n // 1 success per conjugate\r\n if (success) {\r\n successcount++\r\n }\r\n }\r\n\r\n if (successcount === 0) {\r\n return {\r\n success: false\r\n } as const\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount\r\n } as const\r\n}\r\n","import { ALL_CANDIDATES, IndexToNine, INDICES_TO_NINE, SudokuDigits } from \"../../Types\"\r\nimport PureSudoku from \"../Spaces/PureSudoku\"\r\nimport { boxAt } from \"../Utils\"\r\nimport { colorCandidateF } from \"../Utils.dependent\"\r\n\r\n/**\r\n * The state for a candidate in a group\r\n * At the beginning, the state isn't set. This is practically undefined\r\n * undefined = 0 found\r\n * [...] = 1 found, position of hidden single\r\n * false = 2+ found, hidden single not possible anymore\r\n */\r\ntype PossibleState = false | {\r\n row: IndexToNine\r\n column: IndexToNine\r\n}\r\n\r\n/**\r\n * For each group (row, column, or box) there are 9 candidates.\r\n * Digits go from 1 to 9\r\n *\r\n * So each group tracks the possibilities of each digit.\r\n */\r\ntype PossibleGroup = Partial>\r\n\r\n/** A group of rows or columns or boxes. */\r\ntype PossibleGroups = [PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup, PossibleGroup]\r\n\r\nfunction _CreateArrayOf9Groups (): PossibleGroups {\r\n return [\r\n {}, {}, {},\r\n {}, {}, {},\r\n {}, {}, {},\r\n ] as PossibleGroups\r\n}\r\n\r\n\r\nfunction _nextState (currentState: PossibleState | undefined, row: IndexToNine, column: IndexToNine) {\r\n if (currentState === undefined) {\r\n return { row, column }\r\n }\r\n\r\n return false\r\n}\r\n\r\n\r\nexport default function hiddenSingles(sudoku: PureSudoku) {\r\n const possible = {\r\n rows: _CreateArrayOf9Groups(),\r\n columns: _CreateArrayOf9Groups(),\r\n boxes: _CreateArrayOf9Groups(),\r\n }\r\n\r\n for (const row of INDICES_TO_NINE) {\r\n for (const column of INDICES_TO_NINE) {\r\n // Prevent hidden single when already solved\r\n if (sudoku.data[row][column].length === 1) {\r\n const candidate = sudoku.data[row][column][0]\r\n possible.rows[row][candidate] = false\r\n possible.columns[column][candidate] = false\r\n possible.boxes[boxAt(row, column)][candidate] = false\r\n } else {\r\n const box = boxAt(row, column)\r\n for (const candidate of sudoku.data[row][column]) {\r\n possible.rows[row][candidate] = _nextState(possible.rows[row][candidate], row, column)\r\n possible.columns[column][candidate] = _nextState(possible.columns[column][candidate], row, column)\r\n possible.boxes[box][candidate] = _nextState(possible.boxes[box][candidate], row, column)\r\n }\r\n }\r\n }\r\n }\r\n\r\n let successcount = 0\r\n for (const candidate of ALL_CANDIDATES) {\r\n for (let i = 0; i < 9; i++) {\r\n const currentPossible = [\r\n possible.rows[i][candidate],\r\n possible.columns[i][candidate],\r\n possible.boxes[i][candidate],\r\n ]\r\n\r\n for (const cell of currentPossible) {\r\n if (cell !== false && cell !== undefined) {\r\n successcount++\r\n sudoku.set(cell.row, cell.column).to(candidate)\r\n colorCandidateF(sudoku, cell.row, cell.column, candidate, 'solved')\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (successcount !== 0) {\r\n return {\r\n success: true,\r\n successcount\r\n } as const\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { ALL_CANDIDATES, IndexToNine, INDICES_TO_NINE, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { CellID } from \"../Utils\";\r\nimport { colorGroup } from \"../Utils.dependent\";\r\n\r\ntype PossibleNth = Readonly<{\r\n lines: Set[]\r\n sumLines: Set // Set of all cells in the lines\r\n}>\r\n\r\nfunction _innerWingLogic(\r\n candidate: SudokuDigits,\r\n candidateLocations: ReturnType,\r\n sudoku: PureSudoku,\r\n sumLines: Set,\r\n isRow: boolean,\r\n wingSize: number\r\n) {\r\n const patternRows = new Set()\r\n const patternColumns = new Set()\r\n for (const cell of sumLines) {\r\n patternRows.add(cell.row)\r\n patternColumns.add(cell.column)\r\n }\r\n\r\n // Undifferentiate rows and columns\r\n const [patternLines, patternPendLines, lineProp, pendLineProp] =\r\n isRow\r\n ? [patternRows, patternColumns, \"row\", \"column\"] as const\r\n : [patternColumns, patternRows, \"column\", \"row\"] as const\r\n\r\n if (patternPendLines.size <= wingSize) {\r\n // Pattern finally identified!\r\n let success = false\r\n\r\n for (const eliminationPendLine of patternPendLines) {\r\n for (const cell of candidateLocations[candidate][pendLineProp][eliminationPendLine]) {\r\n if (patternLines.has(cell[lineProp]) === false) {\r\n sudoku.remove(candidate).at(cell.row, cell.column)\r\n colorGroup(sudoku, sumLines, candidate)\r\n success = true\r\n }\r\n }\r\n }\r\n\r\n if (success) {\r\n return 1\r\n }\r\n }\r\n\r\n return 0\r\n}\r\n\r\n/**\r\n * Updates the accumulated arrays and sets.\r\n * If an accumulated array reaches the required amount of lines, callback()\r\n */\r\nfunction _accum (line: Set, possibleNLines: PossibleNth[][], index: IndexToNine, size: 2 | 3 | 4, callback: (sumIthLines: Set) => void) {\r\n const optimization = Math.max(index + size - 9, 0) // Example: If index = 8, size = 2,\r\n for (let i = possibleNLines.length - 1; i >= optimization; i--) {\r\n for (const ithLines of possibleNLines[i]) {\r\n const sumIthLines = new Set(ithLines.sumLines)\r\n line.forEach(cell => sumIthLines.add(cell))\r\n\r\n if (sumIthLines.size <= size * size) {\r\n if (i + 1 === size) {\r\n callback(sumIthLines)\r\n } else {\r\n possibleNLines[i + 1].push({\r\n lines: [...ithLines.lines, line],\r\n sumLines: sumIthLines\r\n })\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * See Strategies.md#fish\r\n */\r\nexport default function fish (size: 2 | 3 | 4, sudoku: PureSudoku) {\r\n let successcount = 0\r\n\r\n const candidateLocations = sudoku.getCandidateLocations()\r\n for (const candidate of ALL_CANDIDATES) {\r\n const possibleNRows = [] as PossibleNth[][]\r\n const possibleNColumns = [] as PossibleNth[][]\r\n for (let i = 0; i < size; i++) {\r\n possibleNRows.push([])\r\n possibleNColumns.push([])\r\n }\r\n\r\n for (const index of INDICES_TO_NINE) {\r\n const row = candidateLocations[candidate].row[index]\r\n const column = candidateLocations[candidate].column[index]\r\n\r\n for (const [line, isRow, possibleNLines] of [[row, true, possibleNRows] as const, [column, false, possibleNColumns] as const]) {\r\n if (line.size <= size && line.size > 1) {\r\n // Optimization:\r\n // Say index = 6\r\n // Jellyfish (size 4) can't be made with only 6 7 8\r\n // But it can be with 5 6 7 8\r\n if (9 - index >= size) {\r\n possibleNLines[0].push({\r\n lines: [line],\r\n sumLines: new Set(line)\r\n })\r\n }\r\n\r\n _accum(line, possibleNLines, index, size, sumLines => { // eslint-disable-line no-loop-func\r\n successcount += _innerWingLogic(candidate, candidateLocations, sudoku, sumLines, isRow, size)\r\n })\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (successcount === 0) {\r\n return {\r\n success: false\r\n } as const\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount\r\n } as const\r\n}\r\n","import { GRP_TYPS, INDICES_TO_NINE, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku, { CandidateLocations } from \"../Spaces/PureSudoku\";\r\nimport { SuccessError } from \"../Types\";\r\nimport { affects, assertGet, CellID, id, sharedInArrays } from \"../Utils\";\r\nimport { colorGroup, removeCandidateFromCells } from \"../Utils.dependent\";\r\n\r\nfunction _checkPair(\r\n cellA: CellID,\r\n cellB: CellID,\r\n candidateA: SudokuDigits,\r\n candidateB: SudokuDigits,\r\n affectsCW2C: Map,\r\n candidateLocations: CandidateLocations[],\r\n sudoku: PureSudoku,\r\n) {\r\n const affectsA = assertGet(affectsCW2C, cellA)\r\n const affectsB = assertGet(affectsCW2C, cellB)\r\n const affectsAB = sharedInArrays(affectsA, affectsB)\r\n\r\n // check for a row, column, or box\r\n // which has two cells: an affectsA and an affectsB\r\n for (const prop of GRP_TYPS) {\r\n for (const group of candidateLocations[candidateA][prop]) {\r\n if (group.size === 2) {\r\n const cellxA = affectsA.find(cell => group.has(cell))\r\n const cellxB = affectsB.find(cell => group.has(cell))\r\n\r\n // eslint-disable-next-line sonarjs/no-collapsible-if -- line too long\r\n if (cellxA !== cellxB && cellxA !== undefined && cellxB !== undefined) {\r\n // check for shared cells containing the other candidate\r\n if (removeCandidateFromCells(sudoku, candidateB, affectsAB)) {\r\n colorGroup(sudoku, [cellA, cellB, cellxA, cellxB], candidateA, \"green\")\r\n colorGroup(sudoku, [cellA, cellB], candidateB)\r\n return 1\r\n }\r\n }\r\n }\r\n }\r\n }\r\n return 0\r\n}\r\n\r\nfunction checkPair(\r\n cellA: CellID,\r\n cellB: CellID,\r\n candidateA: SudokuDigits,\r\n candidateB: SudokuDigits,\r\n affectsCW2C: Map,\r\n candidateLocations: CandidateLocations[],\r\n sudoku: PureSudoku,\r\n): number | string {\r\n return _checkPair(cellA, cellB, candidateA, candidateB, affectsCW2C, candidateLocations, sudoku) +\r\n _checkPair(cellA, cellB, candidateB, candidateA, affectsCW2C, candidateLocations, sudoku)\r\n}\r\n\r\n/**\r\n * Calls {@param callback} with all pairs of cells\r\n * whose two candidates are equal\r\n *\r\n * NOTE: You must call another function with (candidateA, candidateB)\r\n * and (candidateB, candidateA)\r\n */\r\nexport function wWingBase (sudoku: PureSudoku, callback: typeof checkPair) {\r\n const found = new Map()\r\n // Delay calculations\r\n const affectsCW2C = new Map()\r\n let candidateLocations\r\n\r\n for (const row of INDICES_TO_NINE) {\r\n for (const column of INDICES_TO_NINE) {\r\n const cell = sudoku.data[row][column]\r\n if (cell.length === 2) {\r\n const numericID = cell[0] * 10 + cell[1]\r\n const equivs = found.get(numericID)\r\n const cid = id(row, column)\r\n affectsCW2C.set(cid, affects(row, column))\r\n if (equivs === undefined) {\r\n found.set(numericID, [cid])\r\n } else {\r\n candidateLocations ??= sudoku.getCandidateLocations()\r\n for (const cell2 of equivs) {\r\n const [candidateA, candidateB] = cell\r\n const successcount =\r\n callback(cid, cell2, candidateA, candidateB, affectsCW2C, candidateLocations, sudoku)\r\n if (typeof successcount === \"string\") {\r\n return {\r\n success: false,\r\n successcount: SuccessError,\r\n message: successcount\r\n }\r\n } else if (successcount) {\r\n return {\r\n success: true,\r\n successcount\r\n } as const\r\n }\r\n }\r\n equivs.push(cid)\r\n }\r\n }\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n\r\n/**\r\n * http://sudopedia.enjoysudoku.com/W-Wing.html\r\n *\r\n * @example\r\n * If the As are strongly linked (and ABs see As)\r\n * then A can be eliminated in the shared AB cells\r\n * ```\r\n * x x x | AB | A\r\n * AB | x x x | A\r\n * ```\r\n */\r\nexport default function wWing (sudoku: PureSudoku) {\r\n return wWingBase(sudoku, checkPair)\r\n}\r\n","import { GRP_TYPS, SudokuDigits, GRP_NAMES } from \"../../Types\";\r\nimport PureSudoku, { CandidateLocations } from \"../Spaces/PureSudoku\";\r\nimport { affects, assertGet, CellID, groupInfo, isSubarray, isSubset, setDifference, sharedInArrays, sharedInSets } from \"../Utils\";\r\nimport { colorGroup, highlightGroup, removeCandidateFromCells } from \"../Utils.dependent\";\r\nimport { wWingBase } from \"./wWing\";\r\n\r\n// TODO: Return information on what kind of success types there are\r\nfunction _checkPair(\r\n cellA: CellID,\r\n cellB: CellID,\r\n candidateA: SudokuDigits,\r\n candidateB: SudokuDigits,\r\n affectsCW2C: Map,\r\n candidateLocations: CandidateLocations[],\r\n sudoku: PureSudoku,\r\n) {\r\n const affectsA = assertGet(affectsCW2C, cellA)\r\n const affectsB = assertGet(affectsCW2C, cellB)\r\n const affectsAandB = sharedInArrays(affectsA, affectsB)\r\n const affectsEitherAorB = new Set(affectsA.concat(affectsB))\r\n\r\n let success = 0\r\n for (const prop of GRP_TYPS) {\r\n for (const group_A of candidateLocations[candidateA][prop]) {\r\n // ^ group [sees A or B] [has A or B]\r\n // Avoid eliminating everything\r\n if (group_A.size === 0) {\r\n const [groupIndex] = groupInfo(prop, group_A)\r\n window._custom.alert(`${prop} ${GRP_NAMES[prop][groupIndex]} has no possibilities for ${candidateA} !`)\r\n return \"error\"\r\n }\r\n\r\n // Condition 2: The two cells see all cells in the group that have A.\r\n if (group_A.isSubsetOf(affectsEitherAorB)) {\r\n // Elimination type A\r\n const [, allOfGroup, remaining] = groupInfo(prop, group_A)\r\n\r\n // Strategy does not work when X or Y is in the group\r\n if (allOfGroup.includes(cellA) || allOfGroup.includes(cellB)) {\r\n continue\r\n }\r\n\r\n // check for shared cells containing the other candidate\r\n if (removeCandidateFromCells(sudoku, candidateB, affectsAandB)) {\r\n highlightGroup(sudoku, remaining, \"orange\")\r\n colorGroup(sudoku, [cellA, cellB], candidateA, \"green\")\r\n colorGroup(sudoku, [cellA, cellB], candidateB)\r\n success++\r\n }\r\n\r\n // Group + sees A or B\r\n // const groupA = sharedInSets(affectsA, group_A) // <\r\n // const groupB = sharedInSets(affectsB, group_A)\r\n\r\n // Group + sees A or B + has A or B\r\n const group_B = [...allOfGroup].filter(cell => sudoku.data[cell.row][cell.column].includes(candidateB))\r\n const groupAA = sharedInSets(affectsA, group_A)\r\n const groupAB = sharedInArrays(affectsA, group_B)\r\n // const groupBA = [...groupB].filter(cell => sudoku.data[cell.row][cell.column].includes(candidateA))\r\n // const groupBB = [...groupB].filter(cell => sudoku.data[cell.row][cell.column].includes(candidateB))\r\n\r\n // Affects A or B + Not in G\r\n const aAnG = setDifference(affectsA, allOfGroup)\r\n\r\n const xSeesY = affectsA.includes(cellB)\r\n const cond3 = isSubset(group_B, affectsEitherAorB)\r\n\r\n // We have\r\n // Z sees X, Z not in GXA (groupAA), X = A or B, Y = A or B\r\n //\r\n // Elim B0\r\n // Z = B --> X = A --> GX != A\r\n //\r\n // Elim B and C (Z sees GXA) + cond 2\r\n // Z = A --> GXA != A --> GY = A --> Y != A\r\n //\r\n // Elim B cond 3\r\n // Z = A --> X = B --> GX != B --> GY = B --> Y != B\r\n //\r\n // Elim C (X sees Y)\r\n // Z = A --> X = B --> Y = A\r\n if (xSeesY || cond3) {\r\n for (const z of aAnG) {\r\n if (z === cellA || z === cellB) {\r\n continue\r\n }\r\n\r\n let affectsZ = affectsCW2C.get(z)\r\n if (affectsZ === undefined) {\r\n affectsZ = affects(z.row, z.column)\r\n affectsCW2C.set(z, affectsZ)\r\n }\r\n\r\n if (isSubarray(groupAA, affectsZ) && removeCandidateFromCells(sudoku, candidateA, [z])) {\r\n highlightGroup(sudoku, remaining, \"orange\")\r\n colorGroup(sudoku, [cellA, cellB], candidateA, \"green\")\r\n colorGroup(sudoku, [cellA, cellB], candidateB)\r\n success++\r\n }\r\n }\r\n }\r\n\r\n // Elim B2 Z sees X + Z sees GXB + cond 2 + cond 3\r\n // Z = B --> X = A --> GX != A --> GY = A --> Y != A\r\n // Z = B --> GXB != B --> GY = B --> Y != B\r\n if (cond3) {\r\n for (const z of aAnG) {\r\n if (z === cellA || z === cellB) {\r\n continue\r\n }\r\n\r\n let affectsZ = affectsCW2C.get(z)\r\n if (affectsZ === undefined) {\r\n affectsZ = affects(z.row, z.column)\r\n affectsCW2C.set(z, affectsZ)\r\n }\r\n\r\n if (isSubarray(groupAB, affectsZ) && removeCandidateFromCells(sudoku, candidateB, [z])) {\r\n highlightGroup(sudoku, remaining, \"salmon\")\r\n colorGroup(sudoku, [cellA, cellB], candidateA, \"green\")\r\n colorGroup(sudoku, [cellA, cellB], candidateB)\r\n success++\r\n }\r\n }\r\n }\r\n\r\n if (success) {\r\n return success\r\n }\r\n }\r\n }\r\n }\r\n return success\r\n}\r\n\r\nfunction checkPair(\r\n cellA: CellID,\r\n cellB: CellID,\r\n candidateA: SudokuDigits,\r\n candidateB: SudokuDigits,\r\n affectsCW2C: Map,\r\n candidateLocations: CandidateLocations[],\r\n sudoku: PureSudoku,\r\n) {\r\n const result1 = _checkPair(cellA, cellB, candidateA, candidateB, affectsCW2C, candidateLocations, sudoku)\r\n const result2 = _checkPair(cellA, cellB, candidateB, candidateA, affectsCW2C, candidateLocations, sudoku)\r\n const result3 = _checkPair(cellB, cellA, candidateA, candidateB, affectsCW2C, candidateLocations, sudoku)\r\n const result4 = _checkPair(cellB, cellA, candidateB, candidateA, affectsCW2C, candidateLocations, sudoku)\r\n\r\n if (result1 === \"error\" || result2 === \"error\" || result3 === \"error\" || result4 === \"error\") {\r\n return \"error\"\r\n }\r\n\r\n return result1 + result2 + result3 + result4\r\n}\r\n\r\n/**\r\n * http://sudopedia.enjoysudoku.com/2-String_Kite.html\r\n * https://github.com/icecream17/solver/wiki/Pair-covers-group\r\n *\r\n * Simultaneously more general than both W-Wing and 2-string kite\r\n *\r\n * This was implemented by pure accident.\r\n *\r\n * @example\r\n *\r\n * ## Elimination A\r\n *\r\n * ```\r\n * AB\r\n * CC CC\r\n * CC CC\r\n *\r\n * AB xx\r\n * ```\r\n *\r\n * It's extended because this is also applied to other groups,\r\n * e.g. rows and columns\r\n * ```\r\n * xx AB\r\n * CC\r\n * CC\r\n *\r\n * AB xx\r\n *\r\n * CC\r\n * CC\r\n * CC\r\n * ```\r\n *\r\n * Example 3\r\n *\r\n * ```\r\n * AB xx xx xx\r\n * xx xx xx AB\r\n * CC CC CC\r\n * ```\r\n *\r\n * Eliminations are in all shared cells not in the group\r\n *\r\n * ## Elimination B\r\n *\r\n * If A and B are not in the C cells,\r\n * A abd B can be eliminated from x\r\n * ```\r\n * | | C C C\r\n * x x x | x x AB |\r\n * x x AB | x x x |\r\n * ```\r\n *\r\n * Example 2\r\n *\r\n * ```\r\n * x x x | AB x x |\r\n * | | C C\r\n * | | C C\r\n * ---------+----------+----------\r\n * | | x\r\n * | | x\r\n * | | AB\r\n * ---------+----------+----------\r\n * | | x\r\n * | | x\r\n * | | x\r\n * ```\r\n */\r\nexport default function pairCoversGroup(sudoku: PureSudoku) {\r\n return wWingBase(sudoku, checkPair)\r\n}\r\n","import { ALL_CANDIDATES, IndexToNine, INDICES_TO_NINE, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, CellID, sharedInArrays } from \"../Utils\";\r\nimport { colorGroup, removeCandidateFromCells } from \"../Utils.dependent\";\r\n\r\nexport function __incrementMapValue, K>(map: T, key: K) {\r\n const value = map.get(key)\r\n if (value === undefined) {\r\n map.set(key, 1)\r\n } else {\r\n map.set(key, value + 1)\r\n }\r\n}\r\n\r\nfunction _innerInnerSkyscraperLogic(\r\n candidate: SudokuDigits,\r\n sudoku: PureSudoku,\r\n sumLines: Set,\r\n perpendProp: \"row\" | \"column\",\r\n wingSize: number\r\n) {\r\n const patternPendLines = new Map()\r\n for (const cell of sumLines) {\r\n __incrementMapValue(patternPendLines, cell[perpendProp])\r\n }\r\n\r\n if (patternPendLines.size === wingSize + 1) {\r\n for (const [eliminationPendLine, count] of patternPendLines) {\r\n if (count > 1) {\r\n const cellsNotInLine = [] as CellID[]\r\n const affectsNotInLine = [] as CellID[][]\r\n for (const cell of sumLines) {\r\n if (cell[perpendProp] !== eliminationPendLine) {\r\n cellsNotInLine.push(cell)\r\n affectsNotInLine.push(affects(cell.row, cell.column))\r\n }\r\n }\r\n\r\n // shared = all extra see\r\n const shared = sharedInArrays(...affectsNotInLine)\r\n\r\n if (removeCandidateFromCells(sudoku, candidate, shared)) {\r\n colorGroup(sudoku, sumLines, candidate)\r\n colorGroup(sudoku, cellsNotInLine, candidate, \"orange\")\r\n return {\r\n success: true,\r\n successcount: 1\r\n } as const\r\n }\r\n }\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\nfunction _innerSkyscraperLogic(line1: Set, possibleLines: Set[], candidate: SudokuDigits, sudoku: PureSudoku, perpendProp: \"row\" | \"column\") {\r\n // line = row/column\r\n // pendLine = column/row\r\n for (const line2 of possibleLines) {\r\n const sumLines = new Set()\r\n line1.forEach(cell => sumLines.add(cell))\r\n line2.forEach(cell => sumLines.add(cell))\r\n\r\n if (sumLines.size < 5) {\r\n const result = _innerInnerSkyscraperLogic(candidate, sudoku, sumLines, perpendProp, 2)\r\n if (result !== null) {\r\n return result\r\n }\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/**\r\n * Disjointed x wing - see Strategies.md\r\n *\r\n * Two lines - 1 cross line = extra\r\n * If all extra see n, n is eliminated (since extra must have at least 1)\r\n */\r\nexport default function skyscraper(sudoku: PureSudoku) {\r\n const candidateLocations = sudoku.getCandidateLocations()\r\n for (const candidate of ALL_CANDIDATES) {\r\n const possibleRows = [] as Set[]\r\n const possibleColumns = [] as Set[]\r\n for (const index of INDICES_TO_NINE) {\r\n const row = candidateLocations[candidate].row[index]\r\n const column = candidateLocations[candidate].column[index]\r\n\r\n if (row.size < 3) {\r\n const result = _innerSkyscraperLogic(row, possibleRows, candidate, sudoku, \"column\")\r\n if (result !== null) {\r\n return result\r\n }\r\n\r\n possibleRows.push(row)\r\n }\r\n\r\n if (column.size < 3) {\r\n const result = _innerSkyscraperLogic(column, possibleColumns, candidate, sudoku, \"row\")\r\n if (result !== null) {\r\n return result\r\n }\r\n\r\n possibleColumns.push(column)\r\n }\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { ALL_CANDIDATES, IndexToNine, INDICES_TO_NINE, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, CellID, sharedInArrays } from \"../Utils\";\r\nimport { colorGroup } from \"../Utils.dependent\";\r\nimport { __incrementMapValue } from \"./skyscraper\";\r\n\r\nfunction __inLine(sumLines: Set, eliminationPendLine: IndexToNine, pendLineProp: \"column\" | \"row\") {\r\n const inLine = new Set()\r\n for (const cell of sumLines) {\r\n if (cell[pendLineProp] === eliminationPendLine) {\r\n inLine.add(cell)\r\n }\r\n }\r\n\r\n return inLine\r\n}\r\n\r\nfunction __updatePatternPendLineElims(\r\n sudoku: PureSudoku,\r\n cell: CellID,\r\n candidate: SudokuDigits,\r\n sumLines: Set,\r\n patternPendLineElims: Map,\r\n eliminationPendLine: IndexToNine\r\n) {\r\n const complexCondition =\r\n sudoku.data[cell.row][cell.column].includes(candidate) &&\r\n !sumLines.has(cell)\r\n\r\n if (complexCondition) {\r\n const recieved = patternPendLineElims.get(cell)\r\n if (recieved === undefined) {\r\n patternPendLineElims.set(cell, [eliminationPendLine])\r\n } else {\r\n patternPendLineElims.set(cell, [...recieved, eliminationPendLine])\r\n }\r\n }\r\n}\r\n\r\nexport function _innerGroupSubtractionLogic(\r\n candidate: SudokuDigits,\r\n sudoku: PureSudoku,\r\n sumLines: Set,\r\n isRow: boolean,\r\n wingSize: number\r\n) {\r\n const patternRows = new Map()\r\n const patternColumns = new Map()\r\n for (const cell of sumLines) {\r\n __incrementMapValue(patternRows, cell.row)\r\n __incrementMapValue(patternColumns, cell.column)\r\n }\r\n\r\n // How many lines see a candidate\r\n const patternPendLineElims = new Map()\r\n const patternPendLines = isRow ? patternColumns : patternRows\r\n const pendLineProp = isRow ? \"column\" : \"row\"\r\n\r\n /**\r\n * A B C | D\r\n * ! |\r\n * ! |\r\n * --------+--\r\n * C | D\r\n *\r\n * A B C | D | D\r\n * A B C | D | D\r\n * ab ab ! | |\r\n * --------+-----+---\r\n * a b C | D | D\r\n * Take each column and ask what they collectively see\r\n * Anything seen by > total - wingSize columns can be eliminated\r\n */\r\n for (const [eliminationPendLine] of patternPendLines) {\r\n const inLine = [] as CellID[][]\r\n for (const cell of sumLines) {\r\n if (cell[pendLineProp] === eliminationPendLine) {\r\n inLine.push(affects(cell.row, cell.column))\r\n }\r\n }\r\n\r\n // shared = all extra see\r\n const shared = sharedInArrays(...inLine)\r\n for (const cell of shared) {\r\n __updatePatternPendLineElims(sudoku, cell, candidate, sumLines, patternPendLineElims, eliminationPendLine)\r\n }\r\n }\r\n\r\n let successcount = 0\r\n let nonExtraLine = null\r\n for (const [cell, linesWhichSee] of patternPendLineElims) {\r\n if (patternPendLines.size - linesWhichSee.length < wingSize) {\r\n const currentNonExtraLine = [...patternPendLines.keys()].find(line => !linesWhichSee.includes(line))\r\n nonExtraLine ??= currentNonExtraLine\r\n if (nonExtraLine === currentNonExtraLine) {\r\n successcount++\r\n sudoku.remove(candidate).at(cell.row, cell.column)\r\n }\r\n }\r\n }\r\n\r\n if (successcount) {\r\n const nonExtraLineCells = __inLine(sumLines, nonExtraLine as IndexToNine, pendLineProp)\r\n const extraCells = sumLines.difference(nonExtraLineCells)\r\n colorGroup(sudoku, extraCells, candidate, \"orange\")\r\n colorGroup(sudoku, nonExtraLineCells, candidate)\r\n return {\r\n success: true,\r\n successcount\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/**\r\n * Disjointed x wing - see Strategies.md\r\n *\r\n * Two lines - 1 cross line = extra\r\n * If all extra see n, n is eliminated (since extra must have at least 1)\r\n */\r\nexport default function twoMinusOneLines(sudoku: PureSudoku) {\r\n const candidateLocations = sudoku.getCandidateLocations()\r\n for (const candidate of ALL_CANDIDATES) {\r\n const possibleRows = [] as Set[]\r\n const possibleColumns = [] as Set[]\r\n for (const index of INDICES_TO_NINE) {\r\n const row = candidateLocations[candidate].row[index]\r\n const column = candidateLocations[candidate].column[index]\r\n\r\n const check = []\r\n if (row.size <= 4) { // 4 cells of a row cannot share anything affects other than the row\r\n check.push([row, possibleRows] as const)\r\n possibleRows.push(row) // Marker 1\r\n }\r\n\r\n if (column.size <= 4) {\r\n check.push([column, possibleColumns] as const)\r\n possibleColumns.push(column) // Marker 1\r\n }\r\n\r\n // line = row/column\r\n // pendLine = column/row\r\n for (const [line1, possibleLines] of check) {\r\n for (const line2 of possibleLines) {\r\n // Necessary because `Marker 1` happens before this\r\n if (line1 === line2) {\r\n continue\r\n }\r\n\r\n const sumLines = new Set()\r\n line1.forEach(cell => sumLines.add(cell))\r\n line2.forEach(cell => sumLines.add(cell))\r\n\r\n const result = _innerGroupSubtractionLogic(candidate, sudoku, sumLines, line1 === row, 2)\r\n if (result !== null) {\r\n return result\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { ALL_CANDIDATES, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku, { CandidateLocations } from \"../Spaces/PureSudoku\";\r\nimport { CellID } from \"../Utils\";\r\nimport { colorCandidateF } from \"../Utils.dependent\";\r\n\r\n/**\r\n * Checks if two cells create a two string kite\r\n * Maybe these checkers could be symbolized as matchers\r\n */\r\nfunction check(cell1: CellID, cell2: CellID, candidate: SudokuDigits, candLocations: CandidateLocations, sudoku: PureSudoku) {\r\n /* eslint-disable sonarjs/no-collapsible-if -- It's clearer */\r\n if (cell1.row === cell2.row || cell1.column === cell2.column) {\r\n return 0\r\n }\r\n\r\n const sameRowAsCell1 = candLocations.row[cell1.row]\r\n const sameColAsCell2 = candLocations.column[cell2.column]\r\n if (sameRowAsCell1.size === 2 && sameColAsCell2.size === 2) {\r\n for (const cell1B of sameRowAsCell1) {\r\n if (cell1B !== cell1) {\r\n for (const cell2B of sameColAsCell2) {\r\n if (cell2B !== cell2) {\r\n // All this does is get 1b and 2b\r\n // 1 1b\r\n // 2\r\n // 2b\r\n if (sudoku.data[cell2B.row][cell1B.column].includes(candidate)) {\r\n colorCandidateF(sudoku, cell1.row, cell1.column, candidate)\r\n colorCandidateF(sudoku, cell2.row, cell2.column, candidate, 'green')\r\n colorCandidateF(sudoku, cell1B.row, cell1B.column, candidate)\r\n colorCandidateF(sudoku, cell2B.row, cell2B.column, candidate, 'green')\r\n sudoku.remove(candidate).at(cell2B.row, cell1B.column)\r\n return 1\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n return 0\r\n}\r\n\r\nexport default function twoStringKite(sudoku: PureSudoku) {\r\n const candidateLocations = sudoku.getCandidateLocations()\r\n\r\n for (const candidate of ALL_CANDIDATES) {\r\n for (const box of candidateLocations[candidate].box) {\r\n if (box.size === 2) {\r\n const [cell1, cell2] = box\r\n const successcount =\r\n check(cell1, cell2, candidate, candidateLocations[candidate], sudoku) +\r\n check(cell2, cell1, candidate, candidateLocations[candidate], sudoku)\r\n if (successcount) {\r\n return {\r\n success: true,\r\n successcount\r\n } as const\r\n }\r\n }\r\n }\r\n }\r\n\r\n return { success: false } as const\r\n}\r\n","import { SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, algebraic, assertGet, CandidateID, CellID, id, removeFromArray, sharedInSets } from \"../Utils\";\r\nimport { colorCandidate, getCellsWithNCandidates } from \"../Utils.dependent\";\r\n\r\n/**\r\n * next = has\r\n *\r\n * See {@link findLoop}\r\n */\r\nexport function cellIsValidLoop (sudoku: PureSudoku, sees: CellID, has: SudokuDigits, loop: CellID[]) {\r\n const cell = sudoku.data[sees.row][sees.column]\r\n return cell.includes(has) && !loop.includes(sees)\r\n}\r\n\r\nfunction seenByColor (sudoku: PureSudoku, color: CandidateID[]) {\r\n const seen = new Set()\r\n for (const { row, column, digit } of color) {\r\n for (const cell of affects(row, column)) {\r\n if (sudoku.data[cell.row][cell.column].includes(digit)) {\r\n seen.add(id(cell.row, cell.column, digit))\r\n }\r\n }\r\n }\r\n\r\n return seen\r\n}\r\n\r\n/**\r\n * Checks if a loop actually eliminates anything\r\n *\r\n * @param endsConnect If ends don't connect, only eliminate from the ends\r\n */\r\nfunction checkLoop (sudoku: PureSudoku, color1: CandidateID[], color2: CandidateID[]) {\r\n const seenByColor1 = seenByColor(sudoku, color1)\r\n const seenByColor2 = seenByColor(sudoku, color2)\r\n const seenByBoth = sharedInSets(seenByColor1, seenByColor2)\r\n\r\n if (seenByBoth.size > 0) {\r\n for (const candidate of color1) {\r\n colorCandidate(sudoku, candidate)\r\n }\r\n for (const candidate of color2) {\r\n colorCandidate(sudoku, candidate, \"green\")\r\n }\r\n for (const { row, column, digit } of seenByBoth) {\r\n sudoku.remove(digit).at(row, column)\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount: 1,\r\n message: `${color2.map(cand => algebraic(cand.row, cand.column)).join(\"\\u200B<>\\u200B\")}\\u200B<>\\u200B`,\r\n } as const\r\n }\r\n\r\n return false\r\n}\r\n\r\n/**\r\n * Looking for a loop of cells\r\n *\r\n * AB, BC, CD, DE, EF, ... and so on, until you reach the end ZA,\r\n * which loops back to AB\r\n *\r\n * In an xyLoop you can be certain that the loop will either be:\r\n *\r\n * ABCDEF...Z\r\n * or\r\n * BCDEF....A\r\n */\r\nexport default function xyLoop (sudoku: PureSudoku) {\r\n /**\r\n * The most important util\r\n *\r\n * @param cell The cell just added to the loop\r\n * @param start The first cell in the loop\r\n * @param next The next cell in the loop needs to have *this* candidate\r\n * @param end The last cell in the loop needs to have *this* candidate\r\n * @param color1 Used for coloring the candidate for display\r\n * @param color2 Used for coloring the candidate for display\r\n * @param loop The current built up loop\r\n * @returns false if failed, CellID[] is loop was found\r\n */\r\n function findLoop (\r\n cell: CellID,\r\n start: CellID,\r\n next: SudokuDigits,\r\n end: SudokuDigits,\r\n color1: CandidateID[],\r\n color2: CandidateID[],\r\n loop: CellID[] = [cell]\r\n ): ReturnType | false {\r\n // All cells AB sees with 2 candidates\r\n const validAffectsCell = __getFellowCWTC(cell).filter(fellow => cellIsValidLoop(sudoku, fellow, next, loop))\r\n\r\n for (const possibleNext of validAffectsCell) {\r\n const nextNext = sudoku.data[possibleNext.row][possibleNext.column].find(\r\n candidate => candidate !== next) as SudokuDigits\r\n\r\n loop.push(possibleNext)\r\n\r\n // No parity check needed, AB BC CD --> ABC BCD\r\n color2.push(id(possibleNext.row, possibleNext.column, next))\r\n color1.push(id(possibleNext.row, possibleNext.column, nextNext))\r\n\r\n const endsConnect = affects(start.row, start.column).includes(possibleNext)\r\n if (nextNext === end && endsConnect) {\r\n const isLoopResult = checkLoop(sudoku, color1, color2)\r\n\r\n if (isLoopResult) {\r\n return isLoopResult\r\n }\r\n }\r\n\r\n const result = findLoop(possibleNext, start, nextNext, end, color1, color2, loop)\r\n if (result) {\r\n return result\r\n }\r\n\r\n loop.pop()\r\n color1.pop()\r\n color2.pop()\r\n }\r\n\r\n return false // Fail\r\n }\r\n\r\n const __getFellowCWTC = (cell: CellID) =>\r\n assertGet(affectsCWTC, cell).filter(sees => cellsWithTwoCandidates.includes(sees))\r\n\r\n\r\n const cellsWithTwoCandidates = getCellsWithNCandidates(sudoku, 2)\r\n\r\n // CWTC acronym for cellsWithTwoCandidates\r\n const affectsCWTC = new Map(\r\n cellsWithTwoCandidates.map(cell => [cell, affects(cell.row, cell.column)])\r\n )\r\n\r\n for (const cell of cellsWithTwoCandidates) {\r\n const [candA, candB] = sudoku.data[cell.row][cell.column]\r\n\r\n // Candidate coloring\r\n const color1 = [id(cell.row, cell.column, candA)] as CandidateID[]\r\n const color2 = [id(cell.row, cell.column, candB)] as CandidateID[]\r\n\r\n // Start with candA\r\n // With a recursive function, add to the list until it fails or succeeds\r\n const result = findLoop(cell, cell, candA, candB, color1, color2)\r\n if (result) {\r\n return result\r\n }\r\n\r\n // Failed, so that cell must not be in any loop, it can be removed\r\n removeFromArray(cell, cellsWithTwoCandidates)\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, algebraic, assertGet, CandidateID, CellID, id, sharedInSets } from \"../Utils\";\r\nimport { colorCandidate, getCellsWithNCandidates, highlightCell } from \"../Utils.dependent\";\r\nimport { cellIsValidLoop } from \"./xyLoop\";\r\n\r\n// Very similar to seenByColor in xyLoop\r\nfunction seenByEnd (sudoku: PureSudoku, { row, column, digit }: CandidateID) {\r\n const seen = new Set()\r\n for (const cell of affects(row, column)) {\r\n if (sudoku.data[cell.row][cell.column].includes(digit)) {\r\n seen.add(id(cell.row, cell.column, digit))\r\n }\r\n }\r\n\r\n return seen\r\n}\r\n\r\n/**\r\n * Checks if a loop (or here, a chain) actually eliminates anything\r\n */\r\nfunction checkLoop (sudoku: PureSudoku, color1: CandidateID[], color2: CandidateID[]) {\r\n const color1End = color1[color1.length - 1]\r\n const color2End = color2[0]\r\n const seenByColor1 = seenByEnd(sudoku, color1End)\r\n const seenByColor2 = seenByEnd(sudoku, color2End)\r\n const seenByBoth = sharedInSets(seenByColor1, seenByColor2)\r\n\r\n if (seenByBoth.size > 0) {\r\n highlightCell(sudoku, id(color1End.row, color1End.column), \"orange\")\r\n highlightCell(sudoku, id(color2End.row, color2End.column), \"orange\")\r\n\r\n for (const candidate of color1) {\r\n colorCandidate(sudoku, candidate)\r\n }\r\n for (const candidate of color2) {\r\n colorCandidate(sudoku, candidate, \"green\")\r\n }\r\n for (const { row, column, digit } of seenByBoth) {\r\n sudoku.remove(digit).at(row, column)\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount: 1,\r\n message: `${color2End.digit} ${color2.map(cand => algebraic(cand.row, cand.column)).join(\"\\u200B<>\\u200B\")} ${color1End.digit}`,\r\n } as const\r\n }\r\n\r\n return false\r\n}\r\n\r\n/**\r\n * Looking for a chain of cells\r\n * The code used is extremely similar to xyLoop\r\n *\r\n * However, the loop doesn't have to be completed.\r\n * There's still the restriction that the first and last cells of the chain must share a candidate\r\n *\r\n * The logic in this case is either:\r\n * first cell = candidate --> not last cell\r\n * last cell = candidate --> not first cell\r\n *\r\n * Basically no matter what, one of the ends has the candidate.\r\n */\r\nexport default function xyChain(sudoku: PureSudoku) {\r\n /**\r\n * The most important util\r\n * Extremely similar to \"findLoop\" in xyLoop\r\n *\r\n * @param cell The cell just added to the loop\r\n * @param next The next cell in the loop needs to have *this* candidate\r\n * @param end The last cell in the loop needs to have *this* candidate\r\n * @param color1 Used for coloring the candidate for display\r\n * @param color2 Used for coloring the candidate for display\r\n * @param loop The current built up loop\r\n * @returns false if failed, CellID[] is loop was found\r\n */\r\n function findLoop(cell: CellID, next: SudokuDigits, end: SudokuDigits, color1: CandidateID[], color2: CandidateID[], loop: CellID[] = [cell]): ReturnType | false {\r\n // All cells AB sees with 2 candidates\r\n const validAffectsCell = __getFellowCWTC(cell).filter(fellow => cellIsValidLoop(sudoku, fellow, next, loop))\r\n\r\n for (const possibleNext of validAffectsCell) {\r\n const nextNext = sudoku.data[possibleNext.row][possibleNext.column].find(\r\n candidate => candidate !== next) as SudokuDigits\r\n\r\n loop.push(possibleNext)\r\n\r\n // No parity check needed, AB BC CD --> ABC BCD\r\n color2.push(id(possibleNext.row, possibleNext.column, next))\r\n color1.push(id(possibleNext.row, possibleNext.column, nextNext))\r\n\r\n if (nextNext === end) {\r\n // Don't care if ends connect\r\n const isLoopResult = checkLoop(sudoku, color1, color2)\r\n\r\n if (isLoopResult) {\r\n return isLoopResult\r\n }\r\n }\r\n\r\n const result = findLoop(possibleNext, nextNext, end, color1, color2, loop)\r\n if (result) {\r\n return result\r\n }\r\n\r\n loop.pop()\r\n color1.pop()\r\n color2.pop()\r\n }\r\n\r\n return false // Fail\r\n }\r\n\r\n const __getFellowCWTC = (cell: CellID) =>\r\n assertGet(affectsCWTC, cell).filter(sees => cellsWithTwoCandidates.includes(sees))\r\n\r\n\r\n const cellsWithTwoCandidates = getCellsWithNCandidates(sudoku, 2)\r\n\r\n // CWTC acronym for cellsWithTwoCandidates\r\n const affectsCWTC = new Map(\r\n cellsWithTwoCandidates.map(cell => [cell, affects(cell.row, cell.column)])\r\n )\r\n\r\n for (const cell of cellsWithTwoCandidates) {\r\n const [candA, candB] = sudoku.data[cell.row][cell.column]\r\n\r\n // Candidate coloring\r\n const color1 = [id(cell.row, cell.column, candA)] as CandidateID[]\r\n const color2 = [id(cell.row, cell.column, candB)] as CandidateID[]\r\n\r\n // Start with candA\r\n // With a recursive function, add to the list until it fails or succeeds\r\n const resultA = findLoop(cell, candA, candB, color1, color2)\r\n if (resultA) {\r\n return resultA\r\n }\r\n\r\n const resultB = findLoop(cell, candB, candA, color2, color1)\r\n if (resultB) {\r\n return resultB\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, assertGet, CellID, sharedInArrays } from \"../Utils\";\r\nimport { getCellsWithNCandidates, colorGroup, removeCandidateFromCells } from \"../Utils.dependent\";\r\n\r\n/**\r\n * The AC cell has A but not B\r\n * The BC cell has B but not A\r\n */\r\nfunction cellIsValidWing(sudoku: PureSudoku, sees: CellID, has: SudokuDigits, notHas: SudokuDigits) {\r\n const cell = sudoku.data[sees.row][sees.column]\r\n return cell.includes(has) && !cell.includes(notHas)\r\n}\r\n\r\nexport default function yWing (sudoku: PureSudoku) {\r\n // AB BC\r\n // AC\r\n\r\n // Looking for a cell AB\r\n // Where A sees another A in AC\r\n // Where B sees another B in BC\r\n\r\n const cellsWithTwoCandidates = getCellsWithNCandidates(sudoku, 2)\r\n\r\n // CWTC acronym for cellsWithTwoCandidates\r\n const affectsCWTC = new Map(\r\n cellsWithTwoCandidates.map(cell => [cell, affects(cell.row, cell.column)])\r\n )\r\n\r\n for (const cell of cellsWithTwoCandidates) {\r\n const [ candA, candB ] = sudoku.data[cell.row][cell.column]\r\n\r\n // All cells AB sees with 2 candidates\r\n const validAffectsCell = assertGet(affectsCWTC, cell).filter(sees => cellsWithTwoCandidates.includes(sees))\r\n\r\n const possibleAC = validAffectsCell.filter(sees => cellIsValidWing(sudoku, sees, candA, candB))\r\n const possibleBC = validAffectsCell.filter(sees => cellIsValidWing(sudoku, sees, candB, candA))\r\n\r\n for (const AC of possibleAC) {\r\n for (const BC of possibleBC) {\r\n if (AC === BC) {\r\n continue;\r\n }\r\n\r\n const cellAC = sudoku.data[AC.row][AC.column]\r\n const cellBC = sudoku.data[BC.row][BC.column]\r\n const candC = cellAC.find(candidate => candidate !== candA) as SudokuDigits\r\n\r\n if (cellBC.includes(candC)) {\r\n // Found a strong link with C in AC and BC!\r\n const sharedEffects = sharedInArrays(\r\n assertGet(affectsCWTC, AC), assertGet(affectsCWTC, BC)\r\n )\r\n\r\n if (removeCandidateFromCells(sudoku, candC, sharedEffects)) {\r\n colorGroup(sudoku, [cell, AC], candA)\r\n colorGroup(sudoku, [cell, BC], candB, \"green\")\r\n colorGroup(sudoku, [AC, BC], candC, \"orange\")\r\n return {\r\n success: true,\r\n successcount: 1\r\n } as const\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","\r\nimport { Strategy } from \"../Types\";\r\nimport checkForSolved from \"./checkForSolved\";\r\nimport hiddenPairsTriplesAndQuads from \"./hiddenPairsTriplesAndQuads\";\r\nimport hiddenSingles from \"./hiddenSingles\";\r\nimport intersectionRemoval from \"./intersectionRemoval\";\r\nimport jellyfish from \"./jellyfish\";\r\nimport pairCoversGroup from \"./pairCoversGroup\";\r\nimport pairsTriplesAndQuads from \"./pairsTriplesAndQuads\";\r\nimport skyscraper from \"./skyscraper\";\r\nimport swordfish from \"./swordfish\";\r\nimport twoMinusOneLines from \"./twoMinusOneLines\";\r\nimport twoStringKite from \"./twoStringKite\";\r\nimport updateCandidates from \"./updateCandidates\";\r\nimport wWing from \"./wWing\";\r\nimport xWing from \"./xWing\";\r\nimport xyChain from \"./xyChain\";\r\nimport xyLoop from \"./xyLoop\";\r\nimport xyzWing from \"./xyzWing\";\r\nimport yWing from \"./yWing\";\r\n\r\n/**\r\n * If testing a particular strategy, import that strategy instead of this array.\r\n * This array is meant to be general not specific.\r\n */\r\nexport default [\r\n checkForSolved,\r\n updateCandidates,\r\n hiddenSingles,\r\n intersectionRemoval,\r\n pairsTriplesAndQuads,\r\n hiddenPairsTriplesAndQuads,\r\n xWing,\r\n swordfish,\r\n jellyfish,\r\n skyscraper,\r\n twoStringKite,\r\n yWing,\r\n twoMinusOneLines,\r\n wWing,\r\n xyzWing,\r\n pairCoversGroup,\r\n xyLoop,\r\n xyChain,\r\n] as Readonly\r\n","import { AlertType, NUMBER_OF_CELLS } from \"../../Types\"\r\nimport PureSudoku from \"../Spaces/PureSudoku\"\r\nimport { StrategyMemory, SuccessError } from \"../Types\"\r\nimport { numberOfCellsWithNCandidates } from \"../Utils.dependent\"\r\nimport checkValidity from \"./checkValidity\"\r\n\r\nexport default function checkForSolved(sudoku: PureSudoku, memory: StrategyMemory[0]) {\r\n const validity = checkValidity(sudoku)\r\n if (!validity.ok) {\r\n window._custom.alert(validity.message, AlertType.ERROR)\r\n return {\r\n success: false,\r\n message: validity.message,\r\n successcount: SuccessError,\r\n } as const\r\n }\r\n\r\n // Should this be before checkValidity?\r\n if (typeof memory.solved !== \"number\") {\r\n throw TypeError(`memory.solved is not a number - got ${String(memory.solved)}`)\r\n } else if (!Number.isInteger(memory.solved)) {\r\n throw TypeError(`memory.solved is not an integer - got ${memory.solved}`)\r\n } else if (0 > memory.solved || memory.solved > NUMBER_OF_CELLS) {\r\n throw TypeError(`impossible amount of memory.solved - got ${memory.solved}`)\r\n }\r\n\r\n const totalSolved = numberOfCellsWithNCandidates(sudoku, 1)\r\n if (totalSolved === NUMBER_OF_CELLS) {\r\n memory.solved = NUMBER_OF_CELLS\r\n return {\r\n success: true,\r\n successcount: NUMBER_OF_CELLS\r\n } as const\r\n }\r\n\r\n if (totalSolved !== memory.solved) {\r\n const difference = totalSolved - memory.solved\r\n memory.solved = totalSolved\r\n\r\n return {\r\n success: true,\r\n successcount: difference\r\n } as const\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { INDICES_TO_NINE } from \"../../Types\"\r\nimport PureSudoku from \"../Spaces/PureSudoku\"\r\nimport { SuccessError } from \"../Types\"\r\nimport { affects, algebraic, CellID } from \"../Utils\"\r\n\r\n// O(n^5)\r\nexport default function updateCandidates(sudoku: PureSudoku) {\r\n let updated = 0\r\n const newResults = new Set()\r\n\r\n for (const i of INDICES_TO_NINE) {\r\n for (const j of INDICES_TO_NINE) {\r\n // Cell\r\n if (sudoku.data[i][j].length === 1) {\r\n\r\n // Cell > Candidate\r\n const solvedCandidate = sudoku.data[i][j][0]\r\n\r\n // Cell > Affects\r\n for (const id of affects(i, j)) {\r\n\r\n // Cell > Affects > Cell\r\n const datacell = sudoku.data[id.row][id.column]\r\n const tempIndex = datacell.indexOf(solvedCandidate)\r\n\r\n // If has candidate\r\n if (tempIndex !== -1) {\r\n // If last candidate of that cell\r\n if (datacell.length === 1) {\r\n return {\r\n success: false,\r\n successcount: SuccessError,\r\n message: `Both ${algebraic(i, j)} and ${algebraic(id.row, id.column)} must be ${solvedCandidate}`\r\n }\r\n }\r\n\r\n datacell.splice(tempIndex, 1) // Deletes the candidate\r\n newResults.add(id)\r\n updated++\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (updated > 0) {\r\n for (const {row, column} of newResults) {\r\n sudoku.set(row, column).to(...sudoku.data[row][column]) // Don't run Cell#setState on every single candidate removal\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount: updated\r\n } as const\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import { ALL_CANDIDATES, INDICES_TO_NINE, SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { boxAt, CellID, removeFromArray } from \"../Utils\";\r\nimport { colorGroup } from \"../Utils.dependent\";\r\n\r\n/**\r\n * If all candidate N in group G attacks range R,\r\n * you can remove N from R\r\n */\r\nexport default function intersectionRemoval(sudoku: PureSudoku) {\r\n const messages = new Set()\r\n\r\n function _removeCandidate(candidate: SudokuDigits, cellID: CellID) {\r\n const newCandidates = sudoku.data[cellID.row][cellID.column]\r\n removeFromArray(candidate, newCandidates)\r\n sudoku.set(cellID.row, cellID.column).to(...newCandidates)\r\n\r\n candidateLocations[candidate].row[cellID.row].delete(cellID)\r\n candidateLocations[candidate].column[cellID.column].delete(cellID)\r\n candidateLocations[candidate].box[boxAt(cellID.row, cellID.column)].delete(cellID)\r\n }\r\n\r\n\r\n function _innerTwoGroupLogic(\r\n candidate: SudokuDigits,\r\n boxLocations: Set,\r\n group2Locations: Set,\r\n boxId: string,\r\n group2Id: string,\r\n ) {\r\n // boxDiff is a copy of boxLocations\r\n const boxDiff = new Set(boxLocations) // Box locations not in line\r\n const group2Diff = new Set() // Row locations not in box\r\n\r\n for (const group2Location of group2Locations) {\r\n if (boxDiff.has(group2Location)) {\r\n boxDiff.delete(group2Location)\r\n } else {\r\n group2Diff.add(group2Location)\r\n }\r\n }\r\n\r\n // In (line and box), but not (rest of box)\r\n if (boxDiff.size === 0 && group2Diff.size !== 0) {\r\n successcount++\r\n colorGroup(sudoku, boxLocations, candidate)\r\n for (const extraCell of group2Diff) {\r\n _removeCandidate(candidate, extraCell)\r\n messages.add(`${candidate}: ${boxId} ⇒ ${group2Id}`)\r\n }\r\n }\r\n\r\n // In (line and box), but not (rest of line)\r\n if (group2Diff.size === 0 && boxDiff.size !== 0) {\r\n successcount++\r\n colorGroup(sudoku, group2Locations, candidate)\r\n for (const extraCell of boxDiff) {\r\n _removeCandidate(candidate, extraCell)\r\n messages.add(`${candidate}: ${group2Id} ⇒ ${boxId}`)\r\n }\r\n }\r\n }\r\n\r\n\r\n let successcount = 0\r\n const candidateLocations = sudoku.getCandidateLocations()\r\n for (const candidate of ALL_CANDIDATES) {\r\n // Boxes vs Rows (and) Boxes vs Columns\r\n for (const boxIndex of INDICES_TO_NINE) {\r\n const boxLocations = candidateLocations[candidate].box[boxIndex]\r\n\r\n for (const groupIndex of INDICES_TO_NINE) {\r\n const rowLocations = candidateLocations[candidate].row[groupIndex]\r\n const columnLocations = candidateLocations[candidate].column[groupIndex]\r\n\r\n if (boxLocations.size < 4 || rowLocations.size < 4) {\r\n _innerTwoGroupLogic(candidate, boxLocations, rowLocations, `Box ${boxIndex + 1}`, `Row ${groupIndex + 1}`)\r\n }\r\n\r\n if (boxLocations.size < 4 || columnLocations.size < 4) {\r\n _innerTwoGroupLogic(candidate, boxLocations, columnLocations, `Box ${boxIndex + 1}`, `Column ${groupIndex + 1}`)\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (successcount === 0) {\r\n return {\r\n success: false\r\n } as const\r\n }\r\n\r\n return {\r\n success: true,\r\n successcount,\r\n message: [...messages].join(\"\\n\")\r\n } as const\r\n}\r\n","import PureSudoku from \"../Spaces/PureSudoku\"\r\nimport fish from \"./fish\";\r\n\r\n/**\r\n * 2 candidates in 2 rows, which align on 2 columns\r\n *\r\n * or\r\n *\r\n * 2 candidates in 2 columns, which align on 2 rows\r\n */\r\nexport default function xWing (sudoku: PureSudoku) {\r\n return fish(2, sudoku)\r\n}\r\n","import PureSudoku from \"../Spaces/PureSudoku\"\r\nimport fish from \"./fish\";\r\n\r\n/**\r\n * Same as xWing, but with 3 lines\r\n */\r\nexport default function swordfish(sudoku: PureSudoku) {\r\n return fish(3, sudoku)\r\n}\r\n","import PureSudoku from \"../Spaces/PureSudoku\";\r\nimport fish from \"./fish\";\r\n\r\n/**\r\n * Same as xWing and swordfish, but with 4 lines\r\n */\r\nexport default function jellyfish(sudoku: PureSudoku) {\r\n return fish(4, sudoku)\r\n}\r\n","import { SudokuDigits } from \"../../Types\";\r\nimport PureSudoku from \"../Spaces/PureSudoku\";\r\nimport { affects, assertGet, boxAt, CellID, sharedInArrays } from \"../Utils\";\r\nimport { getCellsWithNCandidates, highlightCell, removeCandidateFromCells } from \"../Utils.dependent\";\r\n\r\n/* TODO: When finished move to Utils */\r\nfunction inSameBox(cellA: CellID, cellB: CellID) {\r\n return boxAt(cellA.row, cellA.column) === boxAt(cellB.row, cellB.column)\r\n}\r\n\r\n\r\n// /**\r\n// * The AC cell has A but not B\r\n// * The BC cell has B but not A\r\n// */\r\n// function cellIsValidWing (sudoku: PureSudoku, sees: CellID, has: SudokuDigits, notHas: SudokuDigits) {\r\n// const cell = sudoku.data[sees.row][sees.column]\r\n// return cell.includes(has) && !cell.includes(notHas)\r\n// }\r\n\r\nexport default function xyzWing (sudoku: PureSudoku) {\r\n // ABC eliminations | [ABC]{2}\r\n // AC |\r\n\r\n // Looking for ABC\r\n // which is in the same box as an AB\r\n // and also sees another two candidate cell with C (AC or BC)\r\n const cellsWithThreeCandidates = getCellsWithNCandidates(sudoku, 3)\r\n const cellsWithTwoCandidates = getCellsWithNCandidates(sudoku, 2)\r\n\r\n // CW3C acronym for cellsWithTwoCandidates\r\n const affectsCW3C = new Map(\r\n cellsWithThreeCandidates.map(cell => [cell, affects(cell.row, cell.column)])\r\n )\r\n const affectsCW2C = new Map(\r\n cellsWithTwoCandidates.map(cell => [cell, affects(cell.row, cell.column)])\r\n )\r\n\r\n for (const basecell of cellsWithThreeCandidates) {\r\n // AC BC --> ABC\r\n // AC AB --> ABC\r\n const sudokubasecell = sudoku.data[basecell.row][basecell.column]\r\n\r\n // All cells AB sees with 2 candidates\r\n const affectsBaseCell = assertGet(affectsCW3C, basecell)\r\n const validAffectsCell = affectsBaseCell.filter(\r\n sees => cellsWithTwoCandidates.includes(sees) && sudoku.data[sees.row][sees.column].every(candidate => sudokubasecell.includes(candidate))\r\n )\r\n const [valid1stWing, valid2ndWing] = validAffectsCell.reduce<[CellID[], CellID[]]>((accum, sees) => {\r\n if (inSameBox(sees, basecell)) {\r\n accum[0].push(sees)\r\n } else {\r\n accum[1].push(sees)\r\n }\r\n return accum\r\n }, [[], []])\r\n\r\n if (valid1stWing.length === 0 || valid2ndWing.length === 0) {\r\n continue\r\n }\r\n\r\n for (const wing1 of valid1stWing) {\r\n const wing1Cell = sudoku.data[wing1.row][wing1.column]\r\n const extraCandidate = sudokubasecell.find(candidate => !wing1Cell.includes(candidate)) as SudokuDigits\r\n\r\n for (const wing2 of valid2ndWing) {\r\n const wing2Cell = sudoku.data[wing2.row][wing2.column]\r\n if (wing2Cell.includes(extraCandidate) && wing2Cell.every(candidate => sudokubasecell.includes(candidate))) {\r\n const sharedCandidate = sudokubasecell.find(candidate =>\r\n wing1Cell.includes(candidate) && wing2Cell.includes(candidate)\r\n ) as SudokuDigits\r\n\r\n const affectsAll = sharedInArrays(\r\n affectsBaseCell,\r\n assertGet(affectsCW2C, wing1),\r\n assertGet(affectsCW2C, wing2)\r\n )\r\n\r\n if (removeCandidateFromCells(sudoku, sharedCandidate, affectsAll)) {\r\n highlightCell(sudoku, wing1)\r\n highlightCell(sudoku, wing2)\r\n highlightCell(sudoku, basecell, 'orange')\r\n return {\r\n success: true,\r\n successcount: 1\r\n } as const\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n return {\r\n success: false\r\n } as const\r\n}\r\n","import asyncPrompt from \"../asyncPrompt\"\r\nimport EventRegistry from \"../eventRegistry\"\r\nimport { AlertType } from \"../Types\"\r\nimport { forComponentsToUpdate } from \"../utils\"\r\nimport Sudoku from \"./Spaces/Sudoku\"\r\nimport STRATEGIES from \"./Strategies/Strategies\"\r\nimport { SuccessError, StrategyMemory } from \"./Types\"\r\nimport { numberOfCellsWithNCandidates } from \"./Utils.dependent\"\r\n\r\ntype SolverEvents = 'new turn' | 'step finish'\r\n\r\n/**\r\n * Keeps track of the solving strategies, and also implements a few commands\r\n * (Step, Go, Import, Export, Clear).\r\n */\r\nexport default class Solver {\r\n strategyIndex = 0\r\n\r\n /** Later steps wait for earlier steps to finish. Implemented using callback and promises */\r\n whenStepHasFinished: ((stop: boolean) => void)[] = []\r\n isDoingStep = false\r\n\r\n /** Information strategies keep across calls */\r\n memory = new StrategyMemory()\r\n\r\n /**\r\n * Whether a strategy is logically skippable (disabled does not apply).\r\n * Right now, all strategies are skippable if they're retried with no changes to the sudoku.\r\n */\r\n skippable = [] as boolean[]\r\n\r\n /** Which strategies are disabled (used by StrategyItems) */\r\n disabled = [] as boolean[]\r\n\r\n /** Used to reset + update the StrategyItems */\r\n eventRegistry = new EventRegistry()\r\n\r\n constructor(public sudoku: Sudoku) {\r\n // Bind the StrategyControl handlers which have capitzalized names\r\n this.Go = this.Go.bind(this)\r\n this.Step = this.Step.bind(this)\r\n this.Undo = this.Undo.bind(this)\r\n this.Import = this.Import.bind(this)\r\n this.Export = this.Export.bind(this)\r\n this.Clear = this.Clear.bind(this)\r\n }\r\n\r\n updateCounters (success: boolean, errored: boolean, solved: boolean) {\r\n if ((success && this.strategyIndex !== 0) || errored || solved) {\r\n // Go back to the start when a strategy succeeds, errors,\r\n // (or the sudoku is solved, because the user edited it or smth idk)\r\n this.strategyIndex = 0\r\n\r\n this.skippable.fill(false)\r\n\r\n // errored or solved\r\n if (!success) {\r\n return\r\n }\r\n }\r\n\r\n // Usually what happens here is that the strategy fails,\r\n // and is skipped. Which is practically the same as doing one step.\r\n\r\n // Exception: \"check for solved\" isn't really a strategy\r\n else if (this.strategyIndex === 0) {\r\n this.strategyIndex = 1 // sneaky, next if statement will skip \"update candidates\" iff \"check for solved\" failed\r\n this.skippable[0] = true // shouldn't matter, but set for clarity\r\n }\r\n\r\n if (!success) {\r\n this.skippable[this.strategyIndex] = true\r\n }\r\n\r\n while ((this.skippable[this.strategyIndex] || this.disabled[this.strategyIndex]) && this.strategyIndex !== 0) {\r\n this.__step()\r\n }\r\n }\r\n\r\n private __step () {\r\n this.strategyIndex++\r\n if (this.strategyIndex === STRATEGIES.length) {\r\n this.strategyIndex = 0\r\n }\r\n }\r\n\r\n private __promisifyCellMethod (methodName: T & (\"setExplainingToTrue\" | \"setExplainingToFalse\")) {\r\n const promises = [] as Promise[]\r\n\r\n for (const row of this.sudoku.cells) {\r\n for (const cell of row) {\r\n if (cell != null) {\r\n promises.push(new Promise(resolve => {\r\n cell[methodName](resolve)\r\n }))\r\n }\r\n }\r\n }\r\n\r\n return Promise.allSettled(promises)\r\n }\r\n\r\n /**\r\n * !async\r\n */\r\n setupCells () {\r\n return this.__promisifyCellMethod(\"setExplainingToTrue\")\r\n }\r\n\r\n /**\r\n * !async\r\n *\r\n * !misnomer\r\n *\r\n * For each cell, run {@link Cell#setExplainingToFalse}\r\n */\r\n resetCells () {\r\n return this.__promisifyCellMethod(\"setExplainingToFalse\")\r\n }\r\n\r\n /**\r\n * Returns a boolean: \"success\" as in went to next strategy\r\n */\r\n private goToNextStrategyIfDisabled () {\r\n if (this.disabled[this.strategyIndex]) {\r\n this.updateCounters(false, false, false)\r\n return true\r\n }\r\n\r\n return false\r\n }\r\n\r\n private async StartStep (): Promise {\r\n await forComponentsToUpdate()\r\n\r\n this.isDoingStep = true\r\n\r\n // This could theoretically go on forever, but right now the first\r\n // strategy cannot be disabled. TODO: Better solution\r\n\r\n // If current strat is disabled, go to first non-disabled strat.\r\n do {\r\n if (this.strategyIndex === 0) {\r\n this.eventRegistry.notify('new turn')\r\n await this.resetCells()\r\n }\r\n } while (this.goToNextStrategyIfDisabled())\r\n\r\n // Set cells to strategy mode\r\n await this.setupCells()\r\n }\r\n\r\n private async FinishStep (strategyResult: {\r\n success: boolean\r\n successcount: number | null\r\n message: string | null\r\n }) {\r\n // Set cells to non-strategy mode if failed\r\n if (strategyResult.success === false) {\r\n await this.resetCells()\r\n }\r\n\r\n // notify the strategyItem UI\r\n this.eventRegistry.notify('step finish', strategyResult, this.strategyIndex)\r\n await forComponentsToUpdate()\r\n\r\n // \"check for solved\" can return -1 without being an error\r\n // if the user edits the sudoku\r\n const errored = !strategyResult.success && strategyResult.successcount === SuccessError\r\n const solved = numberOfCellsWithNCandidates(this.sudoku, 1) === 81\r\n if (solved) {\r\n window._custom.alert(\"Finished! :D\", AlertType.SUCCESS)\r\n }\r\n\r\n this.updateCounters(strategyResult.success, errored, solved)\r\n this.isDoingStep = false\r\n\r\n return errored || solved\r\n }\r\n\r\n // This is a big function.\r\n // Each comment labels a group of code that does something\r\n\r\n // Originally Promise\r\n async Step (): Promise {\r\n if (this.isDoingStep) {\r\n // Don't do this step yet\r\n // Wait for any previous steps to finish\r\n // After that, continue to the main code\r\n const stop = await new Promise(resolve => {\r\n this.whenStepHasFinished.push(resolve)\r\n })\r\n\r\n this.whenStepHasFinished.shift()\r\n\r\n if (stop) {\r\n this.whenStepHasFinished[0]?.(stop)\r\n return\r\n }\r\n }\r\n\r\n // Main code\r\n await this.StartStep()\r\n\r\n // Run strategy\r\n const _strategyResult = STRATEGIES[this.strategyIndex](this.sudoku, this.memory[this.strategyIndex])\r\n const strategyResult = {\r\n success: _strategyResult.success,\r\n successcount: _strategyResult.successcount ?? null,\r\n message: _strategyResult.message ?? null,\r\n }\r\n\r\n const stop = await this.FinishStep(strategyResult)\r\n\r\n // Do the next step if it's waiting for this one\r\n this.whenStepHasFinished[0]?.(stop)\r\n }\r\n\r\n /** Does \"Step\" until it reaches the end or a strategy succeeds */\r\n async Go () {\r\n do {\r\n await this.Step()\r\n } while (this.strategyIndex !== 0)\r\n }\r\n\r\n async Undo () {\r\n if (this.sudoku === null) return;\r\n for (const row of this.sudoku.cells) {\r\n for (const cell of row) {\r\n cell?.undo(() => {\r\n void this.sudoku\r\n .set(cell.props.row, cell.props.column)\r\n .to(...cell.state.candidates)\r\n })\r\n }\r\n }\r\n this.skippable[this.strategyIndex] = false\r\n await forComponentsToUpdate()\r\n }\r\n\r\n async Import () {\r\n const result = await asyncPrompt(\"Import\", \"Enter digits or candidates\")\r\n if (result === null || result === \"\") {\r\n return; // Don't import on cancel\r\n }\r\n\r\n await this.reset()\r\n this.sudoku.import(result)\r\n }\r\n\r\n Export () {\r\n window._custom.alert(this.sudoku.to81(), undefined, \"monospace\")\r\n window._custom.alert(this.sudoku.to729(), undefined, \"monospace\")\r\n }\r\n\r\n async Clear () {\r\n this.sudoku.clear()\r\n await this.reset()\r\n }\r\n\r\n async reset () {\r\n // BUG: Doesn't wait for steps to finish\r\n await this.resetCells()\r\n this.eventRegistry.notify('new turn')\r\n this.memory = new StrategyMemory()\r\n this.whenStepHasFinished = []\r\n this.strategyIndex = 0\r\n this.skippable = []\r\n }\r\n}\r\n","import { PromptCallback } from \"./Types\"\r\n\r\nexport default function asyncPrompt(title: string, message: string, defaultResult?: string, cssCls?: string): Promise {\r\n return new Promise(resolve => {\r\n window._custom.prompt(title, message, defaultResult, resolve as PromptCallback, cssCls)\r\n })\r\n}\r\n","\r\nimport './AlertNotice.css'\r\nimport React from 'react'\r\nimport Control from '../Control'\r\nimport { AlertType } from '../../Types'\r\n\r\ntype AlertProps = {\r\n whenFinish: () => void\r\n message: string\r\n type: AlertType\r\n cssCls?: string\r\n}\r\n\r\nexport default class AlertNotice extends React.Component {\r\n render () {\r\n const text = [...this.props.message].map((character, index) => (\r\n character === '\\n'\r\n ?

    \r\n : character\r\n ))\r\n return (\r\n
    \r\n

    {text}

    \r\n Ok\r\n
    \r\n )\r\n }\r\n}\r\n","\r\nimport './PromptWindow.css'\r\nimport React from 'react'\r\nimport { PromptCallback } from '../../Types'\r\nimport Control from '../Control'\r\n\r\n// Could also have other props (e.g. \"type\")\r\ntype PromptWindowProps = {\r\n whenFinish: () => void,\r\n\r\n title: string,\r\n message: string,\r\n defaultResponse: string,\r\n callback?: PromptCallback,\r\n cssCls?: string,\r\n}\r\n\r\nexport default class PromptWindow extends React.Component {\r\n inputElement: HTMLTextAreaElement | null = null\r\n setInputElement: (element: HTMLTextAreaElement | null) => HTMLTextAreaElement | null\r\n constructor (props: PromptWindowProps) {\r\n super(props)\r\n this.cancel = this.cancel.bind(this)\r\n this.submit = this.submit.bind(this)\r\n this.setInputElement = (element: HTMLTextAreaElement | null) => this.inputElement = element\r\n }\r\n\r\n render() {\r\n return (\r\n
    \r\n
    \r\n

    {this.props.title}

    \r\n