diff --git a/public/views/1p/jdish.html b/public/views/1p/jdish.html index 0fd06a53..0a586d04 100644 --- a/public/views/1p/jdish.html +++ b/public/views/1p/jdish.html @@ -197,6 +197,29 @@ height: 1080px; object-fit: cover; } + + .srabbit_rating { + font-family: 'Press Start K', monospace; + font-size: 16px; + position: absolute; + width: 80%; + top: 20%; + left: 50%; + transform-origin: center; + text-align: center; + font-size: 24px; + line-height: 28px; + color: white; + text-shadow: -3px -3px 0 black, 3px -3px 0 black, -3px 3px 0 black, + 3px 3px 0 black, 0px -3px 0 black, 0px 3px 0 black, -3px 0px 0 black, + 3px 0px 0 black; + transform: translate(-50%, -50%) scale(0); /* Initial scale 0 */ + will-change: transform; /* Optimize for animation */ + } + + .srabbit_rating p { + padding-bottom: 0.5em; + }
@@ -275,7 +298,7 @@ import QueryString from '/js/QueryString.js'; import '/views/bg.js'; import Connection from '/js/connection.js'; - import { readableScoreFomatter } from '/views/utils.js'; + import { readableScoreFomatter, shuffle } from '/views/utils.js'; import Player from '/views/Player.js'; import { peerServerOptions } from '/views/constants.js'; @@ -319,6 +342,142 @@ } } + // Penner easing + // http://robertpenner.com/easing/ + function easeOutElastic(t, b, c, d) { + var s = 1.70158; + var p = 0; + var a = c; + if (t == 0) return b; + if ((t /= d) >= 1) return b + c; + if (!p) p = d * 0.3; + if (a < Math.abs(c)) { + a = c; + var s = p / 4; + } else var s = (p / (2 * Math.PI)) * Math.asin(c / a); + return ( + a * + Math.pow(2, -10 * t) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p) + + c + + b + ); + } + + function getRandomAngle(min, max) { + const randValue = min + Math.random() * (max - min); + const sign = Math.random() < 0.5 ? 1 : -1; + return randValue * sign; + } + + // From Greg + // <1 point away or equal: best move + // 1-3 points less than top move: excellent + // 3-6 points away: inaccuracy + // 6-15 points away: mistake + // >15 points away: blunder + const srabbit_ratings = { + best: { + score: 4, + feedbacks: [ + 'Fantastic!', + 'Crushed it!', + 'Perfection!', + 'So good!', + 'Great!', + 'Wonderful!', + 'Nailed it!', + 'Top of the game!', + "Couldn't be better!", + 'Masterpiece!', + '10/10, would recommend!', + "You're unstoppable!", + 'Absolutely brilliant!', + 'Flawless victory!', + ], + }, + great: { + score: 3, + feedbacks: [ + 'Damn right!', + 'Booya!', + 'Ooooh yeah!', + "That's what I'm talking about!", + "You're on fire!", + 'Legendary move!', + 'This deserves a trophy!', + 'Rockstar status!', + 'Keep that up!', + 'Winner, winner, chicken dinner!', + "I'm impressed!", + 'Spectacular!', + 'Gold star performance!', + 'Knocked it out of the park!', + 'Epic win!', + ], + }, + notbad: { + score: 2, + feedbacks: [ + 'Well done!', + 'Keep going!', + 'Near perfect!', + 'Pretty good!', + 'Good!', + 'Solid effort!', + 'Almost there!', + 'Nice try!', + "You're getting there!", + 'Not too shabby!', + 'Decent work!', + 'You can be proud of that!', + 'That works!', + 'Smooth sailing!', + 'Keep up the good work!', + ], + }, + oops: { + score: 1, + feedbacks: [ + 'Huho', + 'Errr', + 'You sure?', + "Fractal wouldn't have done that", + 'Interesting?', + 'Dog is laughing!', + 'That was... something', + "Let's pretend that didn't happen", + 'Oopsie daisy!', + 'Close, but no cigar!', + 'Did you trip?', + 'Yikes, try again!', + 'E for effort, I guess?', + 'Swing and a miss!', + "It's... a choice!", + ], + }, + wtf: { + score: 0, + feedbacks: [ + 'Dude WTF!', + 'Serously?', + 'Are you even trying?', + 'Are you joking right now?', + 'AlexT throws a brick your way!', + 'Facepalm!', + 'This is a new low', + "I'm not mad, just disappointed", + 'Wow… just wow', + 'We need to talk', + 'You did WHAT?!', + 'Congratulations, you broke reality', + 'Error: brain.exe not found', + 'Zero points for style', + 'Hahaha!', + ], + }, + }; + const player = new Player( { pb: document.querySelector(`.pbs .pb .content`), @@ -389,11 +548,76 @@ }; player.onGameStart = () => { + streak = 0; player.dom.runway_tr_header.textContent = 'TRANSITION RUNWAY'; player.dom.pb_wrapper.style.color = ''; player.dom.top_wrapper.style.color = ''; }; + let gradeText; + let streak = 0; + let animId = null; + let disappearId = null; + + player.onMoveRating = ({ grade }) => { + if (gradeText) gradeText.remove(); + if (animId) cancelAnimationFrame(animId); + if (disappearId) clearTimeout(disappearId); + + const feedbacks = Object.values(srabbit_ratings).find( + rating => rating.score === grade + ).feedbacks; + if (!feedbacks) return; + + const feedback = shuffle(feedbacks)[0]; + gradeText = document.createElement('div'); + gradeText.classList.add('srabbit_rating'); + + if (grade >= 3) { + gradeText.innerHTML = `${feedback}
${++streak}`; + gradeText.style.color = '#0eff0e'; + } else { + gradeText.style.color = grade >= 1 ? '#ffa500' : '#fd0009'; + gradeText.innerHTML = `${feedback}`; + streak = 0; + } + + player.dom.field.append(gradeText); + + let start = null; + const duration = 800; + const initialScale = 0; + const finalScale = 1; + const randomAngle = getRandomAngle(3, 10); + + function step(timestamp) { + if (!start) start = timestamp; + let progress = timestamp - start; + let easedScale = easeOutElastic( + progress, + initialScale, + finalScale - initialScale, + duration + ); + + if (progress < duration) { + animId = requestAnimationFrame(step); + } else { + // Ensure the final state is exactly scale(1) + easedScale = 1; + disappearId = setTimeout(() => { + disappearId = clearTimeout(disappearId); + gradeText.remove(); + gradeText = null; + }, duration); + } + + gradeText.style.transform = `rotate(${randomAngle}deg) translate(-50%, -50%) scale(${easedScale})`; + } + + animId = requestAnimationFrame(step); + }; + const API = { frame(player_idx, data) { player.setFrame(data); @@ -459,6 +683,8 @@ }); }); }; + + window._player = player; }; diff --git a/public/views/Board.js b/public/views/Board.js index 021da43d..ea88dd84 100644 --- a/public/views/Board.js +++ b/public/views/Board.js @@ -152,4 +152,8 @@ export default class Board { getField() { return this.rows.reduce((acc, row) => (acc.push(...row.cells), acc), []); } + + toString() { + return this.rows.map(row => row.cells.join('')).join('\n'); + } } diff --git a/public/views/Player.js b/public/views/Player.js index f743680f..f0d3d316 100644 --- a/public/views/Player.js +++ b/public/views/Player.js @@ -253,6 +253,7 @@ const DEFAULT_OPTIONS = { const value = QueryString.get('srabbit_playout_length'); return /^[123]$/.test(value) ? parseInt(value, 10) : 2; })(), + srabbit_rate: QueryString.get('srabbit_rate') === '1', curtain: 1, buffer_time, format_score: (v, size) => { @@ -550,6 +551,7 @@ export default class Player extends EventTarget { onGameOver() {} onCurtainDown() {} onTetris() {} + onMoveRating() {} setHideProfileCardOnNextGame(do_hide) { this.hide_profile_card_on_next_game = !!do_hide; @@ -1270,19 +1272,77 @@ export default class Player extends EventTarget { currentPiece: piece_evt.piece, nextPiece: frame.raw.preview, board: piece_evt.field.map(cell => (cell ? 1 : 0)).join(''), - playoutLength: this.options.srabbit_playout_length, + playoutLength: this.options.srabbit_rate + ? 1 + : this.options.srabbit_playout_length, }; const start = Date.now(); - this.stackRabbitWorker.rpc('getMove', params).then(recommendation => { - const elapsed = Date.now() - start; - piece_evt.recommendation = recommendation; - console.log({ - elapsed, - params, - recommendation, - }); - }); + this.stackRabbitWorker + .rpc('getMove', params) + .then(recommendation => { + const elapsed = Date.now() - start; + piece_evt.recommendation = recommendation; + console.log({ + elapsed, + params, + recommendation, + }); + }) + .then(() => { + if (!this.options.srabbit_rate) return null; + + const prior_piece_evt = peek(frame.pieces, 1); + + if (!prior_piece_evt) return null; + + const prior_frame = prior_piece_evt.frame; + + const moveParams = { + level: prior_frame.raw.level <= 18 ? 18 : 19, + lines: prior_frame.raw.lines, + inputFrameTimeline: this.options.srabbit_input_timeline, + currentPiece: prior_piece_evt.piece, + nextPiece: piece_evt.piece, + board: prior_piece_evt.field.map(cell => (cell ? 1 : 0)).join(''), + secondBoard: params.board, + playoutLength: this.options.srabbit_rate + ? 1 + : this.options.srabbit_playout_length, + }; + + this.stackRabbitWorker.rpc('rateMove', moveParams).then(ratings => { + const { playerMoveAfterAdjustment, bestMoveAfterAdjustment } = + ratings; + let grade = null; + + if (playerMoveAfterAdjustment >= bestMoveAfterAdjustment - 1) { + grade = 4; + } else if (playerMoveAfterAdjustment >= bestMoveAfterAdjustment - 3) { + grade = 3; + } else if ( + playerMoveAfterAdjustment >= + bestMoveAfterAdjustment - 6 + ) { + grade = 2; + } else if ( + playerMoveAfterAdjustment >= + bestMoveAfterAdjustment - 15 + ) { + grade = 1; + } + else { + grade = 0; + } + + this.onMoveRating({ params: moveParams, ratings, grade }); + }).catch(err => { + console.error(err); + }) + }) + .catch(err => { + console.error(err); + }) } this.dom.drought.textContent = this.options.format_drought( diff --git a/public/views/stackrabbit/wasmRabbit.wasm b/public/views/stackrabbit/wasmRabbit.wasm index aee869a4..830ab90f 100755 Binary files a/public/views/stackrabbit/wasmRabbit.wasm and b/public/views/stackrabbit/wasmRabbit.wasm differ