From ad329048ef5031cd7f013ace6571a59e9ad4c7a8 Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Wed, 22 Oct 2025 08:53:57 +0300 Subject: [PATCH 01/11] add_new_game_types --- .../app/apps/codebattle/assets/css/style.scss | 8 + .../apps/codebattle/assets/js/i18n/dayjs.js | 13 + .../assets/js/iframes/cssbattle/index.js | 36 + .../app/apps/codebattle/assets/js/socket.js | 3 +- .../assets/js/widgets/components/Editor.jsx | 5 + .../js/widgets/components/EditorGameBar.jsx | 43 + .../js/widgets/components/LanguageIcon.jsx | 32 +- .../js/widgets/config/editor/mongodb.js | 130 ++ .../assets/js/widgets/config/languages.js | 3 + .../assets/js/widgets/initEditor.js | 7 + .../assets/js/widgets/lib/cssbattle.js | 132 ++ .../assets/js/widgets/middlewares/Channel.js | 2 + .../assets/js/widgets/middlewares/Chat.js | 6 +- .../assets/js/widgets/middlewares/Lobby.js | 22 +- .../assets/js/widgets/middlewares/Room.js | 10 +- .../js/widgets/middlewares/Tournament.js | 12 +- .../js/widgets/middlewares/TournamentAdmin.js | 39 +- .../js/widgets/middlewares/WaitingRoom.js | 9 +- .../assets/js/widgets/pages/RoomWidget.jsx | 6 +- .../pages/builder/BuilderEditorsWidget.jsx | 4 +- .../js/widgets/pages/game/ChatWidget.jsx | 14 +- .../widgets/pages/game/CssBattleInfoPanel.jsx | 166 +++ .../js/widgets/pages/game/DarkModeButton.jsx | 11 +- .../js/widgets/pages/game/InfoPanel.jsx | 5 +- .../js/widgets/pages/game/InfoWidget.jsx | 135 +- .../js/widgets/pages/game/Notifications.jsx | 4 +- .../js/widgets/pages/game/SideInfoPanel.jsx | 4 +- .../js/widgets/pages/game/TimerContainer.jsx | 28 +- .../widgets/pages/lobby/GameProgressBar.jsx | 6 +- .../js/widgets/pages/lobby/LobbyWidget.jsx | 35 +- .../pages/lobby/TournamentListItem.jsx | 16 +- .../js/widgets/pages/schedule/EventModal.jsx | 36 +- .../pages/schedule/TournamentSchedule.jsx | 3 +- .../assets/js/widgets/slices/lobby.js | 11 +- .../assets/js/widgets/utils/useCssBattle.js | 251 ++++ .../assets/js/widgets/utils/useEditor.js | 1 - .../lib/codebattle/code_check/css_result.ex | 30 + .../lib/codebattle/code_check/sql_result.ex | 27 + .../codebattle/lib/codebattle/css_task.ex | 89 ++ .../apps/codebattle/lib/codebattle/game.ex | 17 + .../codebattle/lib/codebattle/game/context.ex | 1 + .../codebattle/lib/codebattle/game/engine.ex | 94 +- .../codebattle/lib/codebattle/game/helpers.ex | 2 +- .../codebattle/lib/codebattle/game/player.ex | 67 +- .../lib/codebattle/pub_sub/events.ex | 21 + .../codebattle/lib/codebattle/sql_task.ex | 86 ++ .../apps/codebattle/lib/codebattle/task.ex | 4 + .../lib/codebattle/tournament/player.ex | 8 +- .../apps/codebattle/lib/codebattle/user.ex | 6 + .../codebattle_web/channels/game_channel.ex | 16 + .../codebattle_web/channels/lobby_channel.ex | 28 + .../controllers/api/v1/settings_controller.ex | 8 +- .../css_battle_builder_controller.ex | 9 + .../codebattle/lib/codebattle_web/router.ex | 2 + .../css_battle_builder/app.html.heex | 12 + .../templates/game/game_over.html.slim | 2 +- .../templates/game/game_result.html.heex | 2 +- .../templates/game/join.html.heex | 6 +- .../lib/codebattle_web/views/api/game_view.ex | 64 +- .../views/css_battle_builder_view.ex | 3 + services/app/apps/codebattle/package.json | 3 + .../20251020144944_add_experiment_tasks.exs | 57 + .../codebattle/webpack/webpack.base.config.js | 1 + services/app/apps/codebattle/yarn.lock | 17 + .../app/apps/runner/dockers/css/Dockerfile | 23 + services/app/apps/runner/dockers/css/Makefile | 4 + .../app/apps/runner/dockers/css/checker.js | 114 ++ .../apps/runner/dockers/css/package-lock.json | 1093 +++++++++++++++++ .../app/apps/runner/dockers/css/package.json | 12 + .../apps/runner/dockers/mongodb/Dockerfile | 20 + .../app/apps/runner/dockers/mongodb/Makefile | 3 + .../runner/dockers/mongodb/single_table.js | 10 + .../app/apps/runner/dockers/mysql/Dockerfile | 20 + .../app/apps/runner/dockers/mysql/Makefile | 2 + .../runner/dockers/mysql/single_table.sql | 8 + .../apps/runner/dockers/postgresql/Dockerfile | 19 + .../apps/runner/dockers/postgresql/Makefile | 3 + .../dockers/postgresql/single_table.sql | 8 + .../app/apps/runner/lib/runner/executor.ex | 22 + .../app/apps/runner/lib/runner/languages.ex | 39 + services/app/apps/runner/lib/runner/task.ex | 14 +- 81 files changed, 3151 insertions(+), 193 deletions(-) create mode 100644 services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx create mode 100644 services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx create mode 100644 services/app/apps/codebattle/assets/js/widgets/utils/useCssBattle.js create mode 100644 services/app/apps/codebattle/lib/codebattle/code_check/css_result.ex create mode 100644 services/app/apps/codebattle/lib/codebattle/code_check/sql_result.ex create mode 100644 services/app/apps/codebattle/lib/codebattle/css_task.ex create mode 100644 services/app/apps/codebattle/lib/codebattle/sql_task.ex create mode 100644 services/app/apps/codebattle/lib/codebattle_web/controllers/css_battle_builder_controller.ex create mode 100644 services/app/apps/codebattle/lib/codebattle_web/templates/css_battle_builder/app.html.heex create mode 100644 services/app/apps/codebattle/lib/codebattle_web/views/css_battle_builder_view.ex create mode 100644 services/app/apps/codebattle/priv/repo/migrations/20251020144944_add_experiment_tasks.exs create mode 100644 services/app/apps/runner/dockers/css/Dockerfile create mode 100644 services/app/apps/runner/dockers/css/Makefile create mode 100644 services/app/apps/runner/dockers/css/checker.js create mode 100644 services/app/apps/runner/dockers/css/package-lock.json create mode 100644 services/app/apps/runner/dockers/css/package.json create mode 100644 services/app/apps/runner/dockers/mongodb/Dockerfile create mode 100644 services/app/apps/runner/dockers/mongodb/Makefile create mode 100644 services/app/apps/runner/dockers/mongodb/single_table.js create mode 100644 services/app/apps/runner/dockers/mysql/Dockerfile create mode 100644 services/app/apps/runner/dockers/mysql/Makefile create mode 100644 services/app/apps/runner/dockers/mysql/single_table.sql create mode 100644 services/app/apps/runner/dockers/postgresql/Dockerfile create mode 100644 services/app/apps/runner/dockers/postgresql/Makefile create mode 100644 services/app/apps/runner/dockers/postgresql/single_table.sql diff --git a/services/app/apps/codebattle/assets/css/style.scss b/services/app/apps/codebattle/assets/css/style.scss index f971be63a..b8e1c5a43 100644 --- a/services/app/apps/codebattle/assets/css/style.scss +++ b/services/app/apps/codebattle/assets/css/style.scss @@ -687,6 +687,14 @@ a { opacity: 0; } +.cb-opacity-05 { + opacity: 0.05; +} + +.cb-opacity-10 { + opacity: 0.10; +} + .cb-opacity-25 { opacity: 0.25; } diff --git a/services/app/apps/codebattle/assets/js/i18n/dayjs.js b/services/app/apps/codebattle/assets/js/i18n/dayjs.js index b6274d5b6..181cf0887 100644 --- a/services/app/apps/codebattle/assets/js/i18n/dayjs.js +++ b/services/app/apps/codebattle/assets/js/i18n/dayjs.js @@ -1,10 +1,23 @@ import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import timezone from 'dayjs/plugin/timezone'; +import updateLocale from 'dayjs/plugin/updateLocale'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(duration); +dayjs.extend(updateLocale); + +const locale = dayjs.locale(); + +// const locale = dayjs.locale('es'); +// dayjs.tz.setDefault('Europe/Madrid'); +console.log(`Local: ${dayjs.tz.guess()}`); +/* eslint-disable-next-line */ + +dayjs.updateLocale(locale, { + weekStart: 1, +}); export default dayjs; diff --git a/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js b/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js new file mode 100644 index 000000000..2912f6ef5 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js @@ -0,0 +1,36 @@ +import { toPng } from 'html-to-image'; + +const messageType = 'cssbattle'; + +// if (event.origin.startsWith('https://codebattle.hexlet.io/games/')) { +window.addEventListener( + 'message', + event => { + try { + if (event.data.type !== 'cssbattle') { + return; + } + + window.parent.postMessage( + { type: messageType, data: event.data }, + event.origin, + ); + + if (event.data?.userId) { + const { bodyStr, userId } = event.data; + + document.body.innerHTML = bodyStr; + + toPng(document.body).then(dataUrl => { + window.parent.postMessage( + { type: messageType, dataUrl, userId }, + event.origin, + ); + }); + } + } catch (e) { + console.error(e.message); + } + }, + false, +); diff --git a/services/app/apps/codebattle/assets/js/socket.js b/services/app/apps/codebattle/assets/js/socket.js index f5beb62b5..14c913ffd 100644 --- a/services/app/apps/codebattle/assets/js/socket.js +++ b/services/app/apps/codebattle/assets/js/socket.js @@ -76,13 +76,14 @@ export const channelMethods = { gameScore: 'game:score', gameCancel: 'game:cancel', gameCreate: 'game:create', - cssGameCreate: 'game:css:create', + experimentGameCreate: 'game:experiment:create', gameCreateInvite: 'game:create_invite', gameAcceptInvite: 'game:accept_invite', gameDeclineInvite: 'game:decline_invite', gameCancelInvite: 'game:cancel_invite', reportOnPlayer: 'game:report', + gameTaskChangeTarget: 'game:task:change_target', chatAddMsg: 'chat:add_msg', chatCommand: 'chat:command', diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx index 0b61384a6..5ae9fe294 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import '../initEditor'; import languages from '../config/languages'; +import { actions } from '../slices'; import useEditor from '../utils/useEditor'; import EditorLoading from './EditorLoading'; @@ -105,6 +106,8 @@ Editor.propTypes = { editable: PropTypes.bool, roomMode: PropTypes.string.isRequired, checkResult: PropTypes.func.isRequired, + toggleMuteSound: PropTypes.func, + mute: PropTypes.bool, userType: PropTypes.string.isRequired, userId: PropTypes.number.isRequired, }; @@ -114,8 +117,10 @@ Editor.defaultProps = { lineNumbers: 'on', syntax: 'js', fontSize: 16, + mute: true, editable: false, loading: false, + toggleMuteSound: actions.toggleMuteSound, }; export default memo(Editor); diff --git a/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx b/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx new file mode 100644 index 000000000..b4854d889 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx @@ -0,0 +1,43 @@ +import React, { memo } from 'react'; + +import cn from 'classnames'; +import { useSelector } from 'react-redux'; + +import { getPregressbarClass, getPregressbarWidth } from '@/pages/lobby/GameProgressBar'; + +import EditorThemeCodes from '../config/editorThemes'; +import * as selectors from '../selectors'; + +function EditorGameBar({ userId, theme }) { + const checkResult = useSelector(selectors.executionOutputSelector(userId)); + + const panelClassName = cn('d-flex position-absolute justify-content-center w-100', { + 'bg-white': theme === EditorThemeCodes.light, + 'bg-dark': theme === EditorThemeCodes.dark, + }); + const editorBar = cn( + 'cb-editor-game-progress-bar rounded-bottom bg-light border-top-0', + 'd-flex justify-content-center pb-2 pt-1 px-4', + { + 'bg-light': theme === EditorThemeCodes.light, + 'bg-dark': theme === EditorThemeCodes.dark, + }, + ); + + return ( +
+
+
+
+
+
+
+ ); +} + +export default memo(EditorGameBar); diff --git a/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx b/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx index 25e9ed976..7633fcce1 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx @@ -9,7 +9,10 @@ import GolangOriginalIcon from 'react-devicons/go/original'; import HaskellOriginalIcon from 'react-devicons/haskell/original'; import JavaOriginalIcon from 'react-devicons/java/original'; import LessOriginalIcon from 'react-devicons/less/plain-wordmark'; +import MongodbOriginalIcon from 'react-devicons/mongodb/original'; +import MysqlOriginalIcon from 'react-devicons/mysql/original'; import NodejsPlainIcon from 'react-devicons/nodejs/plain'; +import PostgresqlOriginalIcon from 'react-devicons/postgresql/original'; import SassOriginalIcon from 'react-devicons/sass/original'; import StylusOriginalIcon from 'react-devicons/stylus/original'; import SwiftOriginalIcon from 'react-devicons/swift/original'; @@ -40,9 +43,14 @@ const iconRenderers = { rust: className => , ts: className => , css: className => , - stylus: className => , - less: className => , - sass: className => , + stylus: className => ( + + ), + less: className => , + sass: className => , typescript: className => , java: className => ( ), + postgresql: className => ( + + ), + mongodb: className => ( + + ), + mysql: className => ( + + ), default: () => null, }; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js b/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js new file mode 100644 index 000000000..ba1938f99 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js @@ -0,0 +1,130 @@ +// import tagKeywords from './tagKeywords'; + +const comparisonOperators = [ + 'eq', 'gt', 'gte', 'lt', 'lte', 'ne', 'in', 'nin', 'exists', +]; + +const logicalOperators = [ + 'and', 'not', 'nor', 'or', +]; + +const mongoOperators = [ + 'accumulator', 'addToSet', 'avg', 'bottom', 'bottomN', 'covariancePop', + 'covarianceSamp', 'count', 'derivative', 'denseRank', 'documentNumber', + 'expMovingAvg', 'first', 'firstN', 'integral', 'last', 'lastN', 'max', + 'maxN', 'median', 'min', 'minN', 'percentile', 'push', 'rank', 'stdDevPop', + 'stdDevSamp', 'shift', 'sum', 'top', 'topN', 'locf', 'linearFill', + 'convert', 'ltrim', 'rtrim', 'toBool', 'toDate', 'toDecimal', 'toDouble', + 'toInt', 'toLong', 'toObjectId', 'toString', 'trim', 'abs', 'add', + 'allElementsTrue', 'anyElementTrue', 'arrayElemAt', 'arrayToObject', + 'binarySize', 'bsonSize', 'ceil', 'cmp', 'concat', 'concatArrays', 'cond', + 'dateAdd', 'dateDiff', 'dateFromParts', 'dateFromString', 'dateSubtract', + 'dateToParts', 'dateToString', 'dateTrunc', 'dayOfMonth', 'dayOfWeek', + 'dayOfYear', 'divide', 'exp', 'filter', 'floor', 'function', + 'getField', 'hour', 'ifNull', 'indexOfArray', + 'indexOfBytes', 'indexOfCP', 'isArray', 'isNumber', 'isoDayOfWeek', + 'isoWeek', 'isoWeekYear', 'let', 'literal', 'ln', 'log', 'log10', 'map', + 'mergeObjects', 'meta', 'millisecond', 'minute', 'mod', 'month', 'multiply', + 'objectToArray', 'pow', 'range', 'reduce', 'regexFind', + 'regexFindAll', 'regexMatch', 'replaceAll', 'replaceOne', 'reverseArray', + 'second', 'setDifference', 'setEquals', 'setIntersection', 'setIsSubset', + 'setUnion', 'size', 'slice', 'sortArray', 'split', 'sqrt', 'strcasecmp', + 'strLenBytes', 'strLenCP', 'substr', 'substrBytes', 'substrCP', 'subtract', + 'switch', 'toHashedIndexKey', 'toLower', 'toUpper', 'tsSecond', 'tsIncrement', + 'trunc', 'type', 'week', 'year', 'zip', 'bitAnd', 'bitOr', 'bitXor', + 'bitNot', 'all', 'bitsAllClear', 'bitsAllSet', 'bitsAnyClear', 'bitsAnySet', + 'comment', 'elemMatch', 'expr', 'geoIntersects', 'geoWithin', + 'jsonSchema', 'near', 'nearSphere', + 'regex', 'text', 'where', 'addFields', 'bucket', 'bucketAuto', + 'changeStream', 'collStats', 'currentOp', 'densify', 'documents', 'facet', + 'fill', 'geoNear', 'graphLookup', 'group', 'indexStats', 'limit', + 'listLocalSessions', 'lookup', 'match', 'merge', 'out', 'project', + 'redact', 'replaceRoot', 'replaceWith', 'sample', 'search', 'searchMeta', + 'set', 'setWindowFields', 'skip', 'sort', 'sortByCount', 'unionWith', + 'unset', 'unwind', 'vectorSearch', +]; + +export const languageConfig = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: "'", close: "'", notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] }, + ], + surroundingPairs: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ["'", "'"], + ['"', '"'], + ['`', '`'], + ], + folding: { + markers: { + start: /^\s*\/\/\s*#?region\b/, + end: /^\s*\/\/\s*#?endregion\b/, + }, + }, +}; + +export default { + defaultToken: '', + tagKeywords: [...mongoOperators, ...comparisonOperators, ...logicalOperators], + + tokenizer: { + root: [ + // Comments + [/\/\/.*$/, 'comment'], + [/\/\*/, 'comment', '@comment'], + + // Strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], + [/'([^'\\]|\\.)*$/, 'string.invalid'], + [/"/, 'string', '@string_double'], + [/'/, 'string', '@string_single'], + + // Numbers + [/-?\d*\.\d+([eE][-+]?\d+)?/, 'number.float'], + [/0[xX][0-9a-fA-F]+/, 'number.hex'], + [/-?\d+/, 'number'], + + // Brackets and delimiters + [/[{}()[]]/, '@brackets'], + [/[;,.]/, 'delimiter'], + + // Identifiers + [/[a-zA-Z_]\w*/, 'identifier'], + ], + + comment: [ + [/[^/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[/*]/, 'comment'], + ], + + string_double: [ + [/[^\\"]+/, 'string'], + [/\\./, 'string.escape'], + [/"/, 'string', '@pop'], + ], + + string_single: [ + [/[^\\']+/, 'string'], + [/\\./, 'string.escape'], + [/'/, 'string', '@pop'], + ], + }, + includeLF: false, +}; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/languages.js b/services/app/apps/codebattle/assets/js/widgets/config/languages.js index 2f8316d12..aacfa9c28 100644 --- a/services/app/apps/codebattle/assets/js/widgets/config/languages.js +++ b/services/app/apps/codebattle/assets/js/widgets/config/languages.js @@ -19,6 +19,9 @@ const languages = { less: 'less', sass: 'scss', stylus: 'stylus', + postgresql: 'pgsql', + mongodb: 'mongodb', + mysql: 'mysql', }; export const cssProcessors = ['css', 'less', 'sass', 'stylus']; diff --git a/services/app/apps/codebattle/assets/js/widgets/initEditor.js b/services/app/apps/codebattle/assets/js/widgets/initEditor.js index 832e66520..0008bd54c 100644 --- a/services/app/apps/codebattle/assets/js/widgets/initEditor.js +++ b/services/app/apps/codebattle/assets/js/widgets/initEditor.js @@ -2,6 +2,9 @@ import { loader } from '@monaco-editor/react'; import * as monacoLib from 'monaco-editor'; import haskellProvider from './config/editor/haskell'; +import mongodbProvider, { + languageConfig as mongodbLangConf, +} from './config/editor/mongodb'; import sassProvider from './config/editor/sass'; import stylusProvider from './config/editor/stylus'; @@ -23,4 +26,8 @@ loader.init().then(monaco => { monaco.languages.register({ id: 'scss', aliases: ['scss'] }); monaco.languages.setMonarchTokensProvider('scss', sassProvider); + + monaco.languages.register({ id: 'mongodb', aliases: ['mongodb'] }); + monaco.languages.setMonarchTokensProvider('mongodb', mongodbProvider); + monaco.languages.setLanguageConfiguration('mongodb', mongodbLangConf); }); diff --git a/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js b/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js new file mode 100644 index 000000000..13f9884b0 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js @@ -0,0 +1,132 @@ +import pixelmatch from 'pixelmatch'; + +const diffThreshold = 0.1; + +const getMatchPoints = (stats, width, height) => ( + 1 - stats / (width * height) +); + +const getMatchPercentageText = match => ( + `${(match * 100).toFixed(2)}%` +); + +export const matchBattlePictures = ( + firstImgDataUrl, + secondImgDataUrl, + targetImgDataUrl, + width, + height, +) => { + // 1: Prepare canvases for pixel matching + const firstImgCanvas = document.createElement('canvas'); + const secondImgCanvas = document.createElement('canvas'); + const targetImgCanvas = document.createElement('canvas'); + const firstDiffCanvas = document.createElement('canvas'); + const secondDiffCanvas = document.createElement('canvas'); + + firstImgCanvas.width = width; + firstImgCanvas.height = height; + secondImgCanvas.width = width; + secondImgCanvas.height = height; + targetImgCanvas.width = width; + targetImgCanvas.height = height; + firstDiffCanvas.width = width; + firstDiffCanvas.height = height; + secondDiffCanvas.width = width; + secondDiffCanvas.height = height; + + const firstContext = firstImgCanvas.getContext('2d'); + const secondContext = secondImgCanvas.getContext('2d'); + const targetContext = targetImgCanvas.getContext('2d'); + const firstDiffContext = firstDiffCanvas.getContext('2d'); + const secondDiffContext = secondDiffCanvas.getContext('2d'); + + // 2: Create images from data urls and draw on canvases + + const firstImg = new Image(width, height); + const secondImg = new Image(width, height); + const targetImg = new Image(width, height); + + firstImg.src = firstImgDataUrl; + secondImg.src = secondImgDataUrl; + targetImg.src = targetImgDataUrl; + + firstContext.drawImage(firstImg, 0, 0, width, height); + secondContext.drawImage(secondImg, 0, 0, width, height); + targetContext.drawImage(targetImg, 0, 0, width, height); + + // 3: Pixel match players images on target + + const firstDataImg = firstContext.getImageData(0, 0, width, height); + const secondDataImg = secondContext.getImageData(0, 0, width, height); + const targetDataImg = targetContext.getImageData(0, 0, width, height); + const firstDiff = firstDiffContext.createImageData(width, height); + const secondDiff = secondDiffContext.createImageData(width, height); + + const firstStats = pixelmatch( + firstDataImg.data, + targetDataImg.data, + firstDiff.data, + width, + height, + { threshold: diffThreshold }, + ); + const secondStats = pixelmatch( + secondDataImg.data, + targetDataImg.data, + secondDiff.data, + width, + height, + { threshold: diffThreshold }, + ); + + // 4: Return match statistics + // 4.1: Get match points + + const firstMatchPoints = getMatchPoints(firstStats, width, height); + const secondMatchPoints = getMatchPoints(secondStats, width, height); + + // 4.2: Retrive image data urls from diff canvases + firstDiffContext.putImageData(firstDiff, 0, 0); + secondDiffContext.putImageData(secondDiff, 0, 0); + firstDiffContext.save(); + secondDiffContext.save(); + + const firstDiffDataUrl = firstDiffCanvas.toDataURL('image/png'); + const secondDiffDataUrl = secondDiffCanvas.toDataURL('image/png'); + + // 4.3: Return final result + + const result = [ + { + match: firstMatchPoints, + matchPercentage: getMatchPercentageText(firstMatchPoints), + success: firstMatchPoints >= 0.9999, + diffDataUrl: firstDiffDataUrl, + + // for debug + imageCanvas: firstImgCanvas, + diffCanvas: firstDiffCanvas, + }, + { + match: secondMatchPoints, + matchPercentage: getMatchPercentageText(secondMatchPoints), + success: secondMatchPoints >= 0.9999, + diffDataUrl: secondDiffDataUrl, + + // for debug + imageCanvas: secondImgCanvas, + diffCanvas: secondDiffCanvas, + }, + + ]; + + return { + result, + + // for debug + targetCanvas: targetImgCanvas, + }; +}; + +export default matchBattlePictures; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js index e6926b128..4fea68dbd 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js @@ -205,6 +205,8 @@ export default class Channel { decamelizeKeys(params, { separator: '_' }), ); + pushInstance.receive('error', console.error); + return pushInstance; } diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js index e54618989..f8a72f3d5 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js @@ -58,12 +58,10 @@ export const connectToChat = (useChat = true, chatPage = 'channel', chatId) => d export const addMessage = payload => { channel - .push(channelMethods.chatAddMsg, payload) - .receive('error', error => console.error(error)); + .push(channelMethods.chatAddMsg, payload); }; export const pushCommand = command => { channel - .push(channelMethods.chatCommand, command) - .receive('error', error => console.error(error)); + .push(channelMethods.chatCommand, command); }; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js index ac8ff0121..d1d3605ca 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js @@ -87,42 +87,36 @@ export const openDirect = (userId, name) => dispatch => { export const cancelGame = gameId => () => { channel - .push(channelMethods.gameCancel, { gameId }) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCancel, { gameId }); }; export const createGame = params => { channel - .push(channelMethods.gameCreate, params) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCreate, params); }; -export const createCssGame = params => { +export const createExperimentGame = params => { channel - .push(channelMethods.cssGameCreate, params) + .push(channelMethods.experimentGameCreate, params) .receive('error', error => console.error(error)); }; export const createInvite = invite => { channel - .push(channelMethods.gameCreateInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCreateInvite, invite); }; export const acceptInvite = invite => () => { channel - .push(channelMethods.gameAcceptInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameAcceptInvite, invite); }; export const declineInvite = invite => () => { channel - .push(channelMethods.gameDeclineInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameDeclineInvite, invite); }; export const cancelInvite = invite => () => { channel - .push(channelMethods.gameCancelInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCancelInvite, invite); }; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js index 010621a58..994bc9a79 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js @@ -237,8 +237,7 @@ export const sendEditorText = (editorText, langSlug = null) => (_dispatch, getSt // TODO: only for show tournament export const startRoundTournament = () => { channel - .push('tournament:start_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:start_round', {}); }; export const sendEditorCursorPosition = offset => { @@ -1265,3 +1264,10 @@ export const updateGameHistoryState = nextRecordId => (dispatch, getState) => { break; } }; + +export const changeTaskImgDataUrl = imgDataUrl => () => { + channel.push(channelMethods.gameTaskChangeTarget, { imgDataUrl }) + .receive('ok', () => { + window.location.reload(); + }); +}; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js index bd16ec174..54ba90b9b 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js @@ -194,8 +194,7 @@ export const uploadPlayers = playerIds => (dispatch, getState) => { .push('tournament:players:request', { playerIds }) .receive('ok', response => { dispatch(actions.updateTournamentPlayers(response.players)); - }) - .receive('error', error => console.error(error)); + }); } else { const playerIdsStr = playerIds.join(','); @@ -219,8 +218,7 @@ export const requestMatchesByPlayerId = userId => dispatch => { .receive('ok', data => { dispatch(actions.updateTournamentMatches(data.matches)); dispatch(actions.updateTournamentPlayers(data.players)); - }) - .receive('error', error => console.error(error)); + }); }; export const uploadPlayersMatches = playerId => dispatch => { @@ -230,13 +228,11 @@ export const uploadPlayersMatches = playerId => dispatch => { export const joinTournament = teamId => { const params = teamId !== undefined ? { teamId } : {}; channel - .push('tournament:join', params) - .receive('error', error => console.error(error)); + .push('tournament:join', params); }; export const leaveTournament = teamId => { const params = teamId !== undefined ? { teamId } : {}; channel - .push('tournament:leave', params) - .receive('error', error => console.error(error)); + .push('tournament:leave', params); }; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js index 4313a3c65..ff0f9c648 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js @@ -171,8 +171,7 @@ export const uploadPlayers = playerIds => (dispatch, getState) => { .push('tournament:players:request', { playerIds }) .receive('ok', response => { dispatch(actions.updateTournamentPlayers(response.players)); - }) - .receive('error', error => console.error(error)); + }); } else { const playerIdsStr = playerIds.join(','); @@ -205,8 +204,7 @@ export const requestMatchesByPlayerId = userId => dispatch => { .receive('ok', data => { dispatch(actions.updateTournamentMatches(data.matches)); dispatch(actions.updateTournamentPlayers(data.players)); - }) - .receive('error', error => console.error(error)); + }); }; export const uploadPlayersMatches = playerId => (dispatch, getState) => { @@ -233,14 +231,12 @@ export const uploadPlayersMatches = playerId => (dispatch, getState) => { export const createCustomRound = params => { channel - .push('tournament:start_round', params) - .receive('error', error => console.error(error)); + .push('tournament:start_round', params); }; export const startTournament = () => { channel - .push('tournament:start', {}) - .receive('error', error => console.error(error)); + .push('tournament:start', {}); }; export const cancelTournament = () => dispatch => { @@ -248,57 +244,48 @@ export const cancelTournament = () => dispatch => { .push('tournament:cancel', {}) .receive('ok', response => { dispatch(actions.updateTournamentData(response.tournament)); - }) - .receive('error', error => console.error(error)); + }); }; export const restartTournament = () => { channel - .push('tournament:restart', {}) - .receive('error', error => console.error(error)); + .push('tournament:restart', {}); }; export const startRoundTournament = () => { channel - .push('tournament:start_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:start_round', {}); }; export const finishRoundTournament = () => { channel - .push('tournament:finish_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:finish_round', {}); }; export const toggleVisibleGameResult = gameId => { channel - .push('tournament:toggle_match_visible', { gameId }) - .receive('error', error => console.error(error)); + .push('tournament:toggle_match_visible', { gameId }); }; export const openUpTournament = () => { channel - .push('tournament:open_up', {}) - .receive('error', error => console.error(error)); + .push('tournament:open_up', {}); }; export const showTournamentResults = () => { channel - .push('tournament:toggle_show_results', {}) - .receive('error', error => console.error(error)); + .push('tournament:toggle_show_results', {}); }; export const sendMatchGameOver = matchId => { channel - .push('tournament:match:game_over', { matchId }) - .receive('error', error => console.error(error)); + .push('tournament:match:game_over', { matchId }); }; export const toggleBanUser = (userId, isBanned) => dispatch => { channel .push('tournament:ban:player', { userId }) - .receive('ok', () => dispatch(actions.updateTournamentPlayers([{ id: userId, isBanned }]))) - .receive('error', error => console.error(error)); + .receive('ok', () => dispatch(actions.updateTournamentPlayers([{ id: userId, isBanned }]))); }; export const sendNewReportState = (reportId, state) => dispatch => { diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js index 60baa0a5f..f16daa7b2 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js @@ -134,20 +134,17 @@ export const addWaitingRoomListeners = ( export const pauseWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingPause, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingPause, {}); }; export const startWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingResume, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingResume, {}); }; export const restartWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingRestart, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingRestart, {}); }; export default addWaitingRoomListeners; diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx index 34db8e9a4..f3a677cdb 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx @@ -6,7 +6,7 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group'; import FeedbackAlertNotification from '../components/FeedbackAlertNotification'; import FeedbackWidget from '../components/FeedbackWidget'; -import GameWidgetGuide from '../components/GameWidgetGuide'; +// import GameWidgetGuide from '../components/GameWidgetGuide'; import RoomContext from '../components/RoomContext'; import * as machineSelectors from '../machines/selectors'; import useGameRoomMachine from '../utils/useGameRoomMachine'; @@ -47,7 +47,7 @@ function RoomWidget({ const mute = useGameRoomSoundSettings(); const { - tournamentId, + // tournamentId, viewMode, showWaitingRoom, showBattleRoom, @@ -80,7 +80,7 @@ function RoomWidget({ >
- + {/* */}
- +
- <> - - {openedReplayer ? ( + + {openedReplayer + ? ( )} - {showChatInput && ( - - )} - + {showChatInput && }
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx new file mode 100644 index 000000000..329fa8145 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx @@ -0,0 +1,166 @@ +import React, { + memo, + useCallback, +} from 'react'; + +import cn from 'classnames'; +import { compress } from 'lz-string'; +import { useDispatch } from 'react-redux'; + +import Loading from '@/components/Loading'; +import * as roomActions from '@/middlewares/Room'; +import useCssBattle from '@/utils/useCssBattle'; +import useHover from '@/utils/useHover'; + +const frameStyle = { + minWidth: 300, minHeight: 200, +}; + +const statusTitleMap = { + targetIsEmpty: 'Create a new css task', + targetIsInvalid: 'Error', + process: 'Processing...', + loading: 'Loading...', +}; + +function CssBattleInfoPanel() { + const dispatch = useDispatch(); + + const [ref, hovered] = useHover(); + + const { + matchStats, + leftImgRef, + rightImgRef, + targetImgRef, + leftSolutionIframe, + rightSolutionIframe, + handleLoadLeftIframe, + handleLoadRightIframe, + } = useCssBattle(); + + const handleClick = useCallback(() => { + const compressedDataUrl = compress(leftImgRef.current?.src); + dispatch(roomActions.changeTaskImgDataUrl(compressedDataUrl)); + }, [leftImgRef, dispatch]); + + const isLoading = matchStats.status === 'loading'; + const showStats = hovered && (matchStats.result[0]?.success && matchStats.status !== 'process'); + const showTargetControls = ['targetIsEmpty', 'targetIsInvalid'].includes(matchStats.status); + + return ( +
+
+
+
+ {isLoading && ( +
+ +
+ )} +
+ + +