Browse Source

Add Puzzle

pull/1/head
AlphaX-Projects 3 years ago committed by GitHub
parent
commit
7683003873
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      qortal-ui-plugins/plugins/core/puzzles/index.html
  2. 461
      qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js

45
qortal-ui-plugins/plugins/core/puzzles/index.html

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: #fff;
}
</style>
</head>
<body>
<puzzles-info></puzzles-info>
<script type="module" src="puzzles.js"></script>
</body>
</html>

461
qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js

@ -0,0 +1,461 @@
import { LitElement, html, css } from 'lit-element'
import { render } from 'lit-html'
import { Epml } from '../../../epml.js'
// Not sure if these are imported in the proper way:
import nacl from '../../../../qortal-ui-crypto/api/deps/nacl-fast.js'
import Base58 from '../../../../qortal-ui-crypto/api/deps/Base58.js'
import publicKeyToAddress from '../../../../qortal-ui-crypto/api/wallet/publicKeyToAddress.js'
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-dialog'
import '@material/mwc-slider'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@vaadin/vaadin-grid/vaadin-grid.js'
import '@vaadin/vaadin-grid/theme/material/all-imports.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
const DEFAULT_FEE = 0.001
const PAYMENT_TX_TYPE = 2
class Puzzles extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
invalid: { type: Boolean },
puzzles: { type: Array },
solved: { type: Object },
selectedAddress: { type: Object },
selectedPuzzle: { type: Object },
error: { type: Boolean },
message: { type: String }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
#puzzle-page {
background: #fff;
padding: 12px 24px;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
.red {
--mdc-theme-primary: #F44336;
}
.clue {
font-family: monospaced;
font-size: smaller;
}
`
}
constructor() {
super()
this.loading = false
this.invalid = true
this.puzzles = []
this.solved = {}
this.selectedAddress = {}
this.selectedPuzzle = {}
this.error = false
this.message = ''
}
render() {
return html`
<div id="puzzle-page">
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">Puzzles</h3>
</div>
<vaadin-grid id="puzzlesGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.puzzles)}" .items="${this.puzzles}" height-by-rows>
<vaadin-grid-column auto-width header="Reward" .renderer=${(root, column, data) => {
if (data.item.isSolved) {
render(html`<span style="font-size: smaller;">SOLVED by ${data.item.winner}</span>`, root)
} else {
render(html`<span>${data.item.reward} QORT</span>`, root)
}
}}></vaadin-grid-column>
<vaadin-grid-column auto-width path="name"></vaadin-grid-column>
<vaadin-grid-column auto-width path="description"></vaadin-grid-column>
<vaadin-grid-column auto-width path="clue" style="font-family: monospace; font-size: smaller;"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="Action" .renderer=${(root, column, data) => {
if (data.item.isSolved) {
render(html``, root)
} else {
render(html`<mwc-button @click=${() => this.guessPuzzle(data.item)}><mwc-icon>queue</mwc-icon>Guess</mwc-button>`, root)
}
}}></vaadin-grid-column>
</vaadin-grid>
<mwc-dialog id="puzzleGuessDialog" scrimClickAction="${this.loading ? '' : 'close'}">
<div>Enter your guess to solve this puzzle and win ${this.selectedPuzzle.reward} QORT:</div>
<br>
<div id="puzzleGuessName">Name: ${this.selectedPuzzle.name}</div>
<div id="puzzleGuessDescription">Description: ${this.selectedPuzzle.description}</div>
<div id="puzzleGuessClue" ?hidden=${!this.selectedPuzzle.clue}>Clue: <span class="clue">${this.selectedPuzzle.clue}</span></div>
<br>
<div id="puzzleGuessInputHint" style="font-size: smaller;">Your guess needs to be 43 or 44 characters and <b>not</b> include 0 (zero), I (upper i), O (upper o) or l (lower L).</div>
<mwc-textfield style="width:100%" ?disabled="${this.loading}" label="Your Guess" id="puzzleGuess" pattern="[1-9A-HJ-NP-Za-km-z]{43,44}" style="font-family: monospace;" maxLength="44" charCounter="true" autoValidate="true"></mwc-textfield>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.loading}">
<!-- loading message -->
Claiming your reward... &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.loading}"
alt="Claiming puzzle reward"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.loading || this.invalid}"
slot="primaryAction"
@click=${this.submitPuzzleGuess}>
Submit
</mwc-button>
<mwc-button
?disabled="${this.loading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
</div>
`
}
firstUpdated() {
window.addEventListener("contextmenu", (event) => {
event.preventDefault();
this._textMenu(event)
});
window.addEventListener("click", () => {
parentEpml.request('closeCopyTextMenu', null)
});
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
const textBox = this.shadowRoot.getElementById("puzzleGuess")
// keep track of input validity so we can enabled/disable submit button
textBox.validityTransform = (newValue, nativeValidity) => {
this.invalid = !nativeValidity.valid
return nativeValidity
}
const getPuzzleGroupMembers = async () => {
return await parentEpml.request('apiCall', {
url: `/groups/members/165`
})
}
const getBalance = async(address) => {
return await parentEpml.request('apiCall', {
url: `/addresses/balance/${address}`
})
}
const getName = async(memberAddress) => {
let _names = await parentEpml.request('apiCall', {
url: `/names/address/${memberAddress}`
})
if (_names.length === 0) return "";
return _names[0].name
}
const getNameInfo = async(name) => {
// We have to explicitly encode '#' to stop them being interpreted as in-page references
name = name.replaceAll('#', '%23')
return await parentEpml.request('apiCall', {
url: `/names/${name}`
})
}
const getFirstOutgoingPayment = async(sender) => {
let _payments = await parentEpml.request('apiCall', {
url: `/transactions/search?confirmationStatus=CONFIRMED&limit=20&txType=PAYMENT&address=${sender}`
})
return _payments.find(payment => payment.creatorAddress === sender)
}
const updatePuzzles = async () => {
let _puzzleGroupMembers = await getPuzzleGroupMembers()
let _puzzles = []
await Promise.all(_puzzleGroupMembers.members
.sort((a, b) => b.joined - a.joined)
.map(async (member) => {
let _puzzleAddress = member.member
if (member.isAdmin) return
// Already solved? No need to refresh info
if (this.solved[_puzzleAddress]) {
_puzzles.push(this.solved[_puzzleAddress])
return
}
let _name = await getName(_puzzleAddress)
// No name???
if (_name === "") return
let _reward = await getBalance(_puzzleAddress)
let _isSolved = _reward < 1.0;
let _nameInfo = await getNameInfo(_name)
let _nameData = JSON.parse(_nameInfo.data)
let _puzzle = {
reward: _reward,
address: _puzzleAddress,
name: _name,
description: _nameData.description,
isSolved: _isSolved
}
if (!_isSolved && _nameData.clue)
_puzzle.clue = _nameData.clue;
if (_isSolved) {
// Info on winner
let _payment = await getFirstOutgoingPayment(_puzzleAddress)
_puzzle.winner = _payment.recipient
// Does winner have a name?
let _winnerName = await getName(_puzzle.winner)
if (_winnerName) _puzzle.winner = _winnerName
// Add to 'solved' map to prevent info refresh as it'll never change
this.solved[_puzzleAddress] = _puzzle
}
_puzzles.push(_puzzle);
}))
this.puzzles = _puzzles;
setTimeout(updatePuzzles, 20000)
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
setTimeout(updatePuzzles, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
parentEpml.subscribe('frame_paste_menu_switch', async res => {
res = JSON.parse(res)
if (res.isOpen === false && this.isPasteMenuOpen === true) {
this.pasteToTextBox(textBox)
this.isPasteMenuOpen = false
}
})
})
parentEpml.imReady()
textBox.addEventListener('contextmenu', (event) => {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
// ...
} else {
this.pasteMenu(event)
this.isPasteMenuOpen = true
// Prevent Default and Stop Event Bubbling
event.preventDefault()
event.stopPropagation()
}
}
checkSelectedTextAndShowMenu()
})
}
async guessPuzzle(puzzle) {
this.selectedPuzzle = puzzle
this.shadowRoot.getElementById("puzzleGuess").value = ''
this.shadowRoot.getElementById("puzzleGuess").checkValidity()
this.message = ''
this.invalid = true
this.shadowRoot.querySelector('#puzzleGuessDialog').show()
}
async submitPuzzleGuess(e) {
this.loading = true
this.error = false
// Check for valid guess
const guess = this.shadowRoot.getElementById("puzzleGuess").value
let _rawGuess = Base58.decode(guess)
let _keyPair = nacl.sign.keyPair.fromSeed(_rawGuess)
let _guessAddress = publicKeyToAddress(_keyPair.publicKey)
console.log("Guess '" + _guessAddress + "' vs puzzle's address '" + this.selectedPuzzle.address + "'")
if (_guessAddress !== this.selectedPuzzle.address) {
this.error = true
this.message = 'Guess incorrect!'
this.loading = false
return
}
// Get Last Ref
const getLastRef = async (address) => {
let myRef = await parentEpml.request('apiCall', {
url: `/addresses/lastreference/${address}`
})
return myRef
}
let lastRef = await getLastRef(_guessAddress)
let amount = this.selectedPuzzle.reward - DEFAULT_FEE;
let recipientAddress = this.selectedAddress.address
let txnParams = {
recipient: recipientAddress,
amount: amount,
lastReference: lastRef,
fee: DEFAULT_FEE
}
// Mostly copied from qortal-ui-core/src/plugins/routes.js
let txnResponse = await parentEpml.request('standaloneTransaction', {
type: 2,
keyPair: {
publicKey: _keyPair.publicKey,
privateKey: _keyPair.secretKey
},
params: txnParams
})
if (txnResponse.success) {
this.message = 'Reward claim submitted - check wallet for reward!'
} else {
this.error = true
if (txnResponse.data) {
this.message = "Error while claiming reward: " + txnResponse.data.message
} else {
this.message = "Error while claiming reward: " + txnResponse.message
}
}
this.loading = false
}
pasteToTextBox(textBox) {
// Return focus to the window
window.focus()
navigator.clipboard.readText().then(clipboardText => {
textBox.value += clipboardText
textBox.focus()
});
}
pasteMenu(event) {
let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
parentEpml.request('openFramePasteMenu', eventObject)
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('puzzles-info', Puzzles)
Loading…
Cancel
Save