diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecc7871 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ + +*.lcov + +*.pyc \ No newline at end of file diff --git a/games/__init__.py b/games/__init__.py new file mode 100644 index 0000000..4ceda73 --- /dev/null +++ b/games/__init__.py @@ -0,0 +1 @@ +from .games import GamesXBlock diff --git a/games/games.py b/games/games.py new file mode 100644 index 0000000..e86677c --- /dev/null +++ b/games/games.py @@ -0,0 +1,511 @@ +"""An XBlock providing gamification capabilities.""" +import pkg_resources +from web_fragments.fragment import Fragment +from xblock.core import XBlock + +#May need to import more or less field types later (https://github.com/openedx/XBlock/blob/master/xblock/fields.py) +from xblock.fields import Integer, Scope, String, Boolean, List, Dict + +#need these libraries for random string generation +import string +import random + +class GamesXBlock(XBlock): + """ + An XBlock for creating games. + + The Student view will display the game content and allow the student to interact + accordingly. + + The editor view will allow course authors to create and manipulate the games. + """ + + #Universal fields------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + title = String( + default="Matching", + scope=Scope.content, + help="The title of the block to be displayed in the xblock." + ) + + #Change default to 'matching' for matching game and 'flashcards' for flashcards game to test + type = String( + default="matching", + scope=Scope.settings, + help="The kind of game this xblock is responsible for ('flashcards' or 'matching' for now)." + ) + + #Matching and flashcards will use the same list, but term_image and definition_image will only be used in flashcards + #DUMMY DATA + list = List( + default=[ + { + 'term_image': 'https://studio.stage.edx.org/static/studio/edx.org-next/images/studio-logo.005b2ebe0c8b.png', + 'definition_image': 'https://logos.openedx.org/open-edx-logo-tag.png', + 'term': 'Term 1', + 'definition': 'The definition of term 1 (moderate character length).' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T2', + 'definition': 'Def of T2 - short.' + }, + { + 'term_image': 'https://logos.openedx.org/open-edx-logo-tag.png', + 'definition_image': 'https://studio.stage.edx.org/static/studio/edx.org-next/images/studio-logo.005b2ebe0c8b.png', + 'term': 'The Third Term', + 'definition': 'The definition of term 3. This one is far longer for testing purposes, so long in fact that it should certainly warrant a new line.' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T4', + 'definition': 'D4' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T5', + 'definition': 'D5' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T6', + 'definition': 'D6' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T7', + 'definition': 'D7' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T8', + 'definition': 'D8' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T9', + 'definition': 'D9' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T10', + 'definition': 'D10' + }, + { + 'term_image': None, + 'definition_image': None, + 'term': 'T11', + 'definition': 'D11' + } + ], + scope=Scope.content, + help="The list of terms and definitions." + ) + + list_length = Integer( + default=len(list.default), + scope=Scope.content, + help="A field for the length of the list for convenience." + ) + + #Flashcard game fields------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + list_index = Integer( + default=0, + scope=Scope.settings, + help="Determines which flashcard from the list is currently visible." + ) + + term_is_visible = Boolean( + default=True, + scope=Scope.settings, + help="True when the term is visible and false when the definition is visible in the flashcards game." + ) + + #Matching game fields------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + best_time = Integer( + default=None, + scope=Scope.user_info, + help="The user's best time for the matching game." + ) + + game_started = Boolean( + default = False, + scope=Scope.settings, + help="Bool variable to allow the timer to start from 0 after the game starts." + ) + + time_seconds = Integer( + default=0, + scope=Scope.user_info, + help="The current time elapsed in seconds since starting the matching game." + ) + + selected_containers = Dict( + default={}, + scope=Scope.settings, + help="A dictionary to keep track of selected containers for the matching game." + ) + + matching_id_list = List( + default=[], + scope=Scope.settings, + help="A list of all the matching game ids." + ) + + matching_id_dictionary_index = Dict( + default={}, + scope=Scope.settings, + help="A dictionary to encrypt the ids of the terms and definitions for the matching game." + ) + + matching_id_dictionary_type = Dict( + default={}, + scope=Scope.settings, + help="A dictionary to tie the id to the type of container (term or definition) for the matching game." + ) + + matching_id_dictionary = Dict( + default={}, + scope=Scope.settings, + help="A dictionary to encrypt the ids of the terms and definitions for the matching game." + ) + + match_count = Integer( + default=0, + scope=Scope.settings, + help="Tracks how many matches have been successfully made. Used to determine when to switch pages." + ) + + matches_remaining = Integer( + default = len(list.default), + scope=Scope.content, + help = "The number of matches that remain in the list." + ) + + ''' + #Following fields for editor only------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + shuffle = Boolean( + default=True, + scope=Scope.settings, + help="Whether to shuffle or not. For flashcards only?" + ) + timer = Boolean( + default=True, + scope=Scope.settings, + help="Whether to enable the timer for the matching game." + ) + ''' + + #Important functions (unmodified from xblock installation)------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def student_view(self, context=None): + """ + The primary view of the GamesXBlock, shown to students + when viewing courses. + """ + html = self.resource_string("static/html/games.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/games.css")) + frag.add_javascript(self.resource_string("static/js/src/games.js")) + frag.initialize_js('GamesXBlock') + #frag.initialize_js('FlashcardsXBlock') + return frag + + #Universal handlers------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + @XBlock.json_handler + def expand_game(self, data, suffix=''): + """ + A handler to expand the game from its title block. + """ + description = "ERR: self.type not defined or invalid" + if self.type == "flashcards": + description = "Click each card to reveal the definition" + elif self.type == "matching": + description = "Match each term with the correct definition" + return { + 'title': self.title, + 'description': description, + 'type': self.type + } + + @XBlock.json_handler + def close_game(self, data, suffix=''): + """ + A handler to close the game to its title block. + """ + + self.game_started = False + self.time_seconds = 0 + self.match_count = 0 + self.matches_remaining = self.list_length + + if self.type == "flashcards": + self.term_is_visible=True + self.list_index=0 + return { + 'title': self.title + } + + @XBlock.json_handler + def display_help(self, data, suffix=''): + """ + A handler to display a tooltip message above the help icon. + """ + message = "ERR: self.type not defined or invalid" + if self.type == "flashcards": + message = "Click each card to reveal the definition" + elif self.type == "matching": + message = "Match each term with the correct definition" + return {'message': message} + + #Flashcards handlers------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + @XBlock.json_handler + def start_game_flashcards(self, data, suffix=''): + """ + A handler to begin the flashcards game. + """ + return { + 'list': self.list, + 'list_index': self.list_index, + 'list_length': self.list_length + } + + @XBlock.json_handler + def flip_flashcard(self, data, suffix=''): + """ + A handler to flip the flashcard from term to definition + and vice versa. + """ + if self.term_is_visible: + self.term_is_visible = not(self.term_is_visible) + return {'image': self.list[self.list_index]['definition_image'], 'text': self.list[self.list_index]['definition']} + + self.term_is_visible = not(self.term_is_visible) + return {'image': self.list[self.list_index]['term_image'], 'text': self.list[self.list_index]['term']} + + @XBlock.json_handler + def page_turn(self, data, suffix=''): + """ + A handler to turn the page to a new flashcard (left or right) in the list. + """ + #Always display the term first for a new flashcard. + self.term_is_visible = True + + if data['nextIndex'] == 'left': + if self.list_index>0: + self.list_index-=1 + #else if the current index is 0, circulate to the last flashcard + else: + self.list_index=len(self.list)-1 + return {'term_image': self.list[self.list_index]['term_image'], 'term': self.list[self.list_index]['term'], 'index': self.list_index+1, 'list_length': self.list_length} + + #else data['nextIndex'] == 'right' + if self.list_index + + + + + """), + ("games", + """ + """) + ] + + + + + + + + + + """ + @XBlock.json_handler + def flip_timer(self, data, suffix=''): + self.timer = not(self.timer) + return {'timer': self.timer} + + @XBlock.json_handler + def flip_shuffle(self, data, suffix=''): + self.shuffle = not(self.shuffle) + return {'shuffle': self.shuffle} + """ + + ''' + # The following is another way to approach the list field - currently not used but may be useful after dummy data is no longer used. + default=[ + Dict( + default={'term': 'term1', 'definition': 'definition1'}, + scope=Scope.content, + help="The first flashcard in the list." + ), + Dict( + default={'term': 'term2', 'definition': 'definition2'}, + scope=Scope.content, + help="The second flashcard in the list." + ), + Dict( + default={'term': 'term3', 'definition': 'definition3'}, + scope=Scope.content, + help="The third flashcard in the list." + ) + ], + ''' + #) \ No newline at end of file diff --git a/games/static/README.txt b/games/static/README.txt new file mode 100644 index 0000000..0472ef6 --- /dev/null +++ b/games/static/README.txt @@ -0,0 +1,19 @@ +This static directory is for files that should be included in your kit as plain +static files. + +You can ask the runtime for a URL that will retrieve these files with: + + url = self.runtime.local_resource_url(self, "static/js/lib.js") + +The default implementation is very strict though, and will not serve files from +the static directory. It will serve files from a directory named "public". +Create a directory alongside this one named "public", and put files there. +Then you can get a url with code like this: + + url = self.runtime.local_resource_url(self, "public/js/lib.js") + +The sample code includes a function you can use to read the content of files +in the static directory, like this: + + frag.add_javascript(self.resource_string("static/js/my_block.js")) + diff --git a/games/static/css/games.css b/games/static/css/games.css new file mode 100644 index 0000000..530edc8 --- /dev/null +++ b/games/static/css/games.css @@ -0,0 +1,564 @@ +/* CSS for GamesXBlock */ + +/*Universal styles------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/ +.title-initial { + display: flex; + padding: 18px 281px; + justify-content: center; + align-items: center; + gap: 9px; + + border-radius: 4px; + border: 1px solid var(--light-500, #E1DDDB); + background: #FFF; + + cursor: pointer; +} + +.background-block { + display: flex; + height: 533px; + padding: 24px; + flex-direction: column; + align-items: flex-start; + gap: 24px; + align-self: stretch; + + border-radius: 8px; + background: var(--extras-white, #FFF); + box-shadow: 0px 8px 48px 0px rgba(0, 0, 0, 0.08), 0px 20px 40px 0px rgba(0, 0, 0, 0.08); +} + +.game-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; +} + +.title-persistent { + display: flex; + align-items: center; + gap: 6px; + + color: var(--primary-500, #00262B); + + /* Heading/H3 */ + font-size: 22px; + font-family: Inter; + font-style: normal; + font-weight: 700; + line-height: 28px; +} + +.close-button { + display: flex; + width: 24px; + height: 24px; + justify-content: center; + align-items: center; +} + +.close-background { + display: flex; + width: 36px; + height: 36px; + padding: 6px; + justify-content: center; + align-items: center; + flex-shrink: 0; + + border-radius: 9999999px; +} + +.close-image { + width: 24px; + height: 24px; + flex-shrink: 0; + + cursor: pointer; +} + +.start-block { + display: flex; + padding: 24px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + flex: 1 0 0; + align-self: stretch; + + border-radius: 8px; +} + +.game-description { + color: var(--primary-500, #00262B); + text-align: center; + font-size: 20px; + font-family: Inter; + font-style: normal; + font-weight: 500; + line-height: 28px; +} + +.start-button-flashcards, .start-button-matching { + display: flex; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 8px; + + background: var(--primary-500, #00262B); + + color: #FFF; + font-size: 18px; + font-family: Inter; + font-style: normal; + font-weight: 500; + line-height: 24px; + + cursor: pointer; +} + +.flashcard-footer, .matching-footer { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; +} + +.spacer { + width: 24px; + height: 24px; +} + +.help { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.help-outline { + position: relative; + width: 24px; + height: 24px; + cursor: pointer; +} + +.tooltip { + width: 600%; + position: absolute; + bottom: 140%; + right: -250%; +} + +.tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -8px; + border-width: 8px; + border-style: solid; + border-color: var(--core-elm, #00262B) transparent transparent transparent; +} + +.tooltip-text { + color: var(--extras-white, #FFF); + text-align: center; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + + display: flex; + padding: 4px 8px; + flex-direction: column; + align-items: center; + gap: 10px; + + border-radius: 4px; + background: var(--core-elm, #00262B); +} + +@keyframes confetti { + 0% { transform: translate(0, 0); opacity: 1; } + 100% { transform: translate(0vw, 85vh) rotate(720deg); opacity: 0; } +} + +.confetti { + position: fixed; + top: 0; + pointer-events: none; + opacity: 0; +} + +/*Flashcard styles------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/ +.flashcard-block { + display: flex; + height: 373px; + padding: 24px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + flex-shrink: 0; + align-self: stretch; + + border-radius: 8px; + border: 2px solid var(--light-300, #F2F0EF); + + cursor: pointer; +} + +.image { + max-height: 200px; + flex-shrink: 0; + + /*background: url(), lightgray 0px -18.962px / 100% 118.363% no-repeat;*/ +} + +.card-text { + color: var(--primary-500, #00262B); + /*leading-trim: both; + text-edge: cap; + */ + font-size: 18px; + font-family: Inter; + font-style: normal; + font-weight: 400; + line-height: 28px; +} + +.flashcard-navigation { + display: flex; + align-items: center; + gap: 16px; +} + +.flashcard-left-button, .flashcard-right-button { + display: flex; + width: 36px; + height: 36px; + padding: 6px; + justify-content: center; + align-items: center; + + border-radius: 999999984306749400px; + background: #F2F0EF; + + cursor: pointer; +} + +.flashcard-left-image, .flashcard-right-image { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.flashcard-navigation-text { + color: var(--primary-500, #00262B); + text-align: center; + /*leading-trim: both; + text-edge: cap; + */ + font-size: 20px; + font-family: Inter; + font-style: normal; + font-weight: 500; + line-height: normal; +} + +/*Matching styles------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/ +.matching-block { + display: flex; + height: 610px; + align-items: flex-start; + gap: 8px; + align-self: stretch; +} + +.matching-column-l, .matching-column-r { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + flex: 1 0 0; + align-self: stretch; +} + +@keyframes incorrect { + 0% {border: 2px solid var(--danger-500, #AB0D02); background-color: #FCF1F4}; + 100% {border: initial; background-color: initial}; +} + +.matching-container, .matching-container-empty { + display: flex; + padding: 16px; + justify-content: center; + align-items: center; + gap: 10px; + flex: 1 0 0; + align-self: stretch; + + flex-direction: column; + + border-radius: 8px; + border: 2px solid var(--light-300, #F2F0EF); + + color: var(--primary-500, #00262B); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + + animation-duration: 1.3s; + + cursor: pointer; +} + +@keyframes correct { + 0% {border: 2px solid var(--success-500, #0D7D4D); background-color: var(--success-100, #F2FAF7); color: var(--primary-500, #00262B)}; + 100% {border: initial; background-color: initial; color: initial};; +} + +.matching-container-empty { + cursor: initial; + color: white; + user-select: none; + border: 2px solid #ffffff; +} + +.matching-progress-container { + display: flex; + width: 48px; + height: 48px; + border-radius: 4px; + background: #fff; + flex-direction: column; + align-items: center; +} + +.matching-progress-indicator { + position: relative; + height: 48px; + width: 48px; + border-radius: 50%; + background: conic-gradient(#0d7d4d 0deg, #f2f0ef 0deg); + display: flex; + align-items: center; + justify-content: center; +} +.matching-progress-indicator::before { + content: ""; + position: absolute; + height: 40px; + width: 40px; + border-radius: 50%; + background-color: #fff; +} + +.matching-progress-text { + position: relative; + color: var(--primary-500, #00262B); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 17px; + font-style: normal; + font-weight: 500; + line-height: 28px; + letter-spacing: -1.19px; +} + +.matching-timer { + color: var(--primary-500, #00262B); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 28px; + font-style: normal; + font-weight: 500; + line-height: 28px; + letter-spacing: -1.96px; +} + +.matching-end-block { + display: flex; + padding: 0px 77px; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 1 0 0; + align-self: stretch; +} + +.matching-end-congratulations { + color: var(--primary-700, #002121); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 32px; + font-style: normal; + font-weight: 700; + line-height: 36px; /* 112.5% */ +} + +.matching-replay-button { + display: flex; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 8px; + + background: var(--primary-500, #00262B); + + color: #FFF; + font-family: Inter; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 133.333% */ + + cursor: pointer; +} + +.matching-end-standard-text-block { + display: flex; + height: 610px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 32px; +} + +.matching-end-standard-text, .matching-end-standard-time { + color: var(--primary-500, #00262B); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 22px; + font-style: normal; + font-weight: 500; + line-height: 28px; /* 127.273% */ +} + +.matching-end-standard-time { + font-weight: 700; +} + +.matching-end-standard-best-block { + display: flex; + padding: 19px 24px; + justify-content: center; + align-items: center; + gap: 10px; + + border-radius: 8px; + background: rgba(240, 204, 0, 0.20); +} + +.matching-standard-award { + width: 28px; + height: 28px; +} + +.matching-end-standard-best-text, .matching-end-standard-best-time { + color: var(--extras-black, #000); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 22px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.matching-end-standard-best-time { + text-align: right; +} + +.matching-record-text-block { + display: inline-flex; + height: 610px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 32px; +} + +.matching-end-record-time, .matching-end-record-message { + color: var(--primary-500, #00262B); + text-align: center; + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 88px; + font-style: normal; + font-weight: 600; + line-height: 28px; /* 31.818% */ +} + +.matching-end-record-message { + font-size: 28px; + font-weight: 700; +} + +.matching-end-record-previous-block { + display: flex; + padding: 16px; + justify-content: center; + align-items: center; + gap: 10px; + + border-radius: 8px; + background: rgba(242, 240, 239, 0.60); +} + +.matching-end-record-previous-text, .matching-end-record-previous-time { + color: var(--gray-700, #454545); + /* + leading-trim: both; + text-edge: cap; + */ + font-family: Inter; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +.matching-end-record-previous-time { + text-align: right; +} + +.matching-end-record-confetti-block { + width: 616.382px; + height: 685.116px; +} \ No newline at end of file diff --git a/games/static/html/games.html b/games/static/html/games.html new file mode 100644 index 0000000..f0e035d --- /dev/null +++ b/games/static/html/games.html @@ -0,0 +1,6 @@ + +

+

+
{self.title}
+
+

\ No newline at end of file diff --git a/games/static/js/src/games.js b/games/static/js/src/games.js new file mode 100644 index 0000000..155d50a --- /dev/null +++ b/games/static/js/src/games.js @@ -0,0 +1,596 @@ +/* Javascript for GamesXBlock. */ + +function GamesXBlock(runtime, element) { + //Universal functions------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + //Expand Game + function expandGame(fullView) { + //Function to expand the game from its title block + + $('.title-initial', element).remove(); + + var expandedBlock = "
"; + var topDiv = "
"; + var title = "
"; + var closeButton = "
"; + var closeBackground = "
"; + var closeImage = "" + var startBlock = "
"; + var description = "
"; + + //Depending on the game type, add the respective start button; add new cases for new game types + switch(fullView.type){ + case 'flashcards': var startButton = "
Start
"; break; + case 'matching': var startButton = "
Start
"; break; + default: var startButton = "
ERR: invalid type
"; + } + + $('.gamesxblock', element).append(expandedBlock); + $('.background-block', element).append(topDiv); + $('.game-top', element).append(title); + $('.title-persistent', element).text(fullView.title); + $('.game-top', element).append(closeButton); + $('.close-button', element).append(closeBackground); + $('.close-background', element).append(closeImage); + $('.background-block', element).append(startBlock); + $('.start-block', element).append(description); + $('.game-description', element).text(fullView.description); + $('.start-block', element).append(startButton); + } + $(document).on('click', '.title-initial', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'expand_game'), + data: JSON.stringify({}), + success: expandGame + }); + }); + + //Close Game + function closeGame(initialView) { + //Function to close the current game back to the title block + + var init = "
"; + + $('.background-block', element).remove(); + $('.gamesxblock', element).append(init); + $('.title-initial', element).text(initialView.title); + } + $(document).on('click', '.close-image', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'close_game'), + data: JSON.stringify({}), + success: closeGame + }); + }) + + //Help + function getHelp(help) { + //Function to show the game's tooltip + + var tooltip = "
"; + var tooltipText = "
"; + + $('.help-outline', element).append(tooltip); + $('.tooltip', element).append(tooltipText); + $('.tooltip-text', element).text(help.message); + } + function hideHelp(help) { + //Function to hide the game's tooltip + + $('.tooltip', element).remove(); + } + $(document).on('mouseenter', '.help', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'display_help'), + data: JSON.stringify({}), + success: getHelp + }); + }); + $(document).on('mouseleave', '.help', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'display_help'), + data: JSON.stringify({}), + success: hideHelp + }); + }); + + //Flashcards functions------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + //Start Flashcards + function startFlashcards(firstCard) { + //Function to start the flashcards game + + $('.start-block', element).remove(); + + var flashcardBlock = "
"; + var firstImage = ""; + var text = "
"; + var footer = ""; + var spacer = "
"; + var help = "
"; + var helpOutline = "
"; + var navigation = "
"; + var left = "
"; + var leftImage = ""; + var navText = "
"; + var right = "
"; + var rightImage = ""; + + $('.background-block', element).append(flashcardBlock); + $('.flashcard-block', element).append(firstImage); + $('.image', element).attr("src", firstCard.list[firstCard.list_index]['term_image']); + $('.flashcard-block', element).append(text); + $('.flashcard-text', element).text(firstCard.list[firstCard.list_index]['term']); + $('.background-block', element).append(footer); + $('.flashcard-footer', element).append(spacer); + $('.flashcard-footer', element).append(navigation); + $('.flashcard-footer', element).append(help); + $('.help', element).append(helpOutline); + $('.flashcard-navigation', element).append(left); + $('.flashcard-left-button', element).append(leftImage); + $('.flashcard-navigation', element).append(navText); + $('.flashcard-navigation-text', element).text("1" + " / " + firstCard.list_length); + $('.flashcard-navigation', element).append(right); + $('.flashcard-right-button', element).append(rightImage); + } + $(document).on('click', '.start-button-flashcards', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'start_game_flashcards'), + data: JSON.stringify({}), + success: startFlashcards + }); + }); + + //Flip Flashcard + function flipFlashcard(newSide) { + //Function to flip the current flashcard + + $('.image', element).attr("src", newSide.image); + $('.flashcard-text', element).text(newSide.text); + } + $(document).on('click', '.flashcard-block', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'flip_flashcard'), + data: JSON.stringify({}), + success: flipFlashcard + }); + }); + + //Turn Page + function pageTurn(nextCard) { + //Function to turn the page to another flashcard + + $('.image', element).attr("src", nextCard.term_image); + $('.flashcard-text', element).text(nextCard.term); + $('.flashcard-navigation-text', element).text(nextCard.index + " / " + nextCard.list_length); + } + $(document).on('click', '.flashcard-left-button', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'page_turn'), + data: JSON.stringify({nextIndex: 'left'}), + success: pageTurn + }); + }); + $(document).on('click', '.flashcard-right-button', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'page_turn'), + data: JSON.stringify({nextIndex: 'right'}), + success: pageTurn + }); + }); + + //Matching functions------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + //Start Matching + function startMatching(firstPage) { + //Function to start the matching game + + $('.start-block', element).remove(); + + //the css style changes made by jquery are not persistent after the element is removed, so the following line does not need to be undone when closeGame is called + $('.background-block', element).css({"height":"initial", "padding":"20px 24px 24px 24px"}); + + var matchingBlock = "
"; + var matchingColumnL = "
" + var matchingColumnR = "
"; + + $('.background-block', element).append(matchingBlock); + $('.matching-block', element).append(matchingColumnL); + $('.matching-block', element).append(matchingColumnR); + + //progress shown by default, but if there are 5 or less pairs, it will not be displayed + var showProgress = true; + + //add the first five items to the left column + for (let i=0; i<5; i++) { + //if there are no more terms to add, use empty containers instead to account for formatting + if (i>2*firstPage.list_length-1) { + showProgress = false; + var emptyContainer = "
"; + $('.matching-column-l', element).append(emptyContainer); + continue; + } + var id = firstPage.id_list[i]; + var content = firstPage.id_dictionary[id] + var container = "
" + content + "
"; + $('.matching-column-l', element).append(container); + } + //add the next five items to the right column + for (let i=5; i<10; i++) { + //if there are no more terms to add, use empty containers instead to account for formatting + if (i>2*firstPage.list_length-1) { + showProgress = false; + var emptyContainer = "
"; + $('.matching-column-r', element).append(emptyContainer); + continue; + } + var id = firstPage.id_list[i]; + var content = firstPage.id_dictionary[id] + var container = "
" + content + "
"; + $('.matching-column-r', element).append(container); + } + + var footer = ""; + var progressContainer = "
"; + var progressIndicator = "
"; + var progressText = "
"; + var timer = "
" + firstPage.time + "
"; + var help = "
"; + var helpOutline = "
"; + + $('.background-block', element).append(footer); + //add progress container regardless for formatting + $('.matching-footer', element).append(progressContainer); + if(showProgress) { + var pageCount = Math.floor((firstPage.list_length-1)/5)+1; + var currentProgressDeg = 360*(1/pageCount); + var progressIndicatorStyle = "conic-gradient(#0d7d4d " + currentProgressDeg + "deg, #f2f0ef 0deg"; + $('.matching-progress-container', element).append(progressIndicator); + $('.matching-progress-indicator', element).append(progressText); + $('.matching-progress-indicator', element).css("background", progressIndicatorStyle) + $('.matching-progress-text', element).text("1 " + "/" + " " + pageCount); + } + $('.matching-footer', element).append(timer); + $('.matching-footer', element).append(help); + $('.help', element).append(helpOutline); + } + $(document).on('click', '.start-button-matching', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'start_game_matching'), + data: JSON.stringify({}), + success: startMatching + }); + }); + + //Timer + function formatTime(timeSeconds) { + //Function to format the time_seconds field (for use by other functions) + + var seconds = ":" + timeSeconds%60; + if (timeSeconds%60 < 10) + var seconds = ":0" + timeSeconds%60; + + var minutes = Math.floor(timeSeconds/60)%60; + + var hours = ""; + if (timeSeconds >= 3600) { + var hours = Math.floor(timeSeconds/3600)%24 + ":"; + if (Math.floor(timeSeconds/60)%60 < 10) + hours = Math.floor(timeSeconds/3600)%24 + ":0"; + } + + //returns as a string formatted as (HH:M)M:SS, where (HH:M) only appear if they are relevant + return hours + minutes + seconds; + } + function updateTimer(newTime) { + //Function to update the timer + + //do nothing until the game starts + if (!newTime.game_started) + return; + $('.matching-timer', element).text(formatTime(newTime.value)); + } + $(document).ready(function() { + //Update the timer every 1000 ms + setInterval(function() { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'update_timer'), + data: JSON.stringify({}), + cache: false, + success: updateTimer + }); + }, 1000); + }); + + //Select Container + function selectContainer(selected) { + //Function to handle selecting containers displaying either a term or a definition (also covers matches and deselcting) + + //first selection; when no other container is selected, select the container that was clicked + if (selected.first_selection) { + //cancel all animations if another container is selected in case they are not finished yet + $('.matching-container', element).css("animation-name", "initial"); + + $('.matching-container', element).css("border", "2px solid var(--light-300, #F2F0EF)"); + $('.matching-container', element).css("background-color", "initial"); + $(selected.id).css("border", "2px solid var(--primary-500, #00262B)"); + return; + } + + //deselect; when a selected container is selected again, deselect it + if (selected.deselect) { + $(selected.id).css("border", "2px solid var(--light-300, #F2F0EF)"); + $(selected.id).css("background-color", "initial"); + return; + } + + //false match; if the second container selected does not match the first container, run the 'incorrect' animation on the two selected then deselect them + if (!selected.match) { + //reset the previously selected element's border before the animation so that it does not appear selected after the animation + $(selected.prev_id).css("border", "2px solid var(--light-300, #F2F0EF)"); + + $(selected.id).css({"animation-name": "incorrect"}); + $(selected.prev_id).css({"animation-name": "incorrect"}); + + //if the animation-name is not reset in the 'first selection' section above, the animations will only be able to fire once + return; + } + + //match; if the second container selected matches the first container, change their classes to 'matching-container-empty' and run the 'correct' animation + //reset the borders to white to prevent the dark border to persist + $(selected.id).css("border", "2px solid #ffffff"); + $(selected.prev_id).css("border", "2px solid #ffffff"); + //replace the class and apply the correct animation + $(selected.id).removeClass("matching-container"); + $(selected.prev_id).removeClass("matching-container"); + $(selected.id).addClass("matching-container-empty"); + $(selected.prev_id).addClass("matching-container-empty"); + $(selected.id).css({"animation-name": "correct"}); + $(selected.prev_id).css({"animation-name": "correct"}); + + //next page; every 5 matches, display the next 5 pairs or whatever remains + if (selected.match_count%5 == 0 && selected.matches_remaining > 0) { + //clear the page + for(let i=2*selected.match_count-10; i<2*selected.match_count; i++) { + id = "#" + selected.id_list[i]; + $(id).remove(); + } + //add the next 5 items to the left column + for (let i=2*selected.match_count; i<2*selected.match_count+5; i++) { + //if there are no more terms to add, use empty containers instead to account for formatting + if (i>2*selected.list_length-1) { + var emptyContainer = "
"; + $('.matching-column-l', element).append(emptyContainer); + continue; + } + var id = selected.id_list[i]; + var content = selected.id_dictionary[id] + var container = "
" + content + "
"; + $('.matching-column-l', element).append(container); + } + //add the next 5 items to the right column + for (let i=2*selected.match_count+5; i<2*selected.match_count+10; i++) { + //if there are no more terms to add, use empty containers instead to account for formatting + if (i>2*selected.list_length-1) { + var emptyContainer = "
"; + $('.matching-column-r', element).append(emptyContainer); + continue; + } + var id = selected.id_list[i]; + var content = selected.id_dictionary[id] + var container = "
" + content + "
"; + $('.matching-column-r', element).append(container); + } + + //if the progress indicator exists, it will be updated, otherwise this will simply have no effect + var currentProgress = Math.floor(selected.match_count/5+1); + var pageCount = Math.floor((selected.list_length-1)/5)+1 + var currentProgressDeg = 360*(currentProgress/pageCount); + var progressIndicatorStyle = "conic-gradient(#0d7d4d " + currentProgressDeg + "deg, #f2f0ef 0deg"; + $('.matching-progress-indicator', element).css("background", progressIndicatorStyle) + $('.matching-progress-text', element).text(currentProgress + " " + "/" + " " + pageCount); + } + + //end game; after the last match is made, end the game with an ajax call + if (selected.matches_remaining == 0) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'end_game_matching'), + data: JSON.stringify({newTime: selected.time_seconds}), + success: endGameMatching + }); + } + } + + //End Matching Game + function endGameMatching(lastPage) { + //function for ajax to end matching game + + //changes made regardless (new record or not) + $('.matching-block', element).remove(); + $('.matching-footer', element).remove(); + + var endBlock = "
"; + var endFooter = ""; + + $('.background-block', element).append(endBlock); + $('.background-block', element).append(endFooter); + $('.matching-footer', element).css("height", "48px"); + + //new record; Show confetti and new best + if (lastPage.new_record) { + var textBlock = "
"; + var congrats = "
Congratulations!
"; + var newTime = "
" + formatTime(lastPage.new_time) + "
"; + var message = "
A new personal best!
"; + //only display previous time if it exists + if (lastPage.prev_time != null) { + var previousBlock = "
"; + var previousText = "
Previous best:
"; + var previousTime = "
" + formatTime(lastPage.prev_time) + "
"; + } + var replayButton = "
Play again
"; + + $('.matching-end-block', element).append(textBlock); + $('.matching-record-text-block', element).append(congrats); + $('.matching-record-text-block', element).append(newTime); + $('.matching-record-text-block', element).append(message); + //only display previous time if it exists + if (lastPage.prev_time != null) { + $('.matching-record-text-block', element).append(previousBlock); + $('.matching-end-record-previous-block', element).append(previousText); + $('.matching-end-record-previous-block', element).append(previousTime); + } + $('.matching-record-text-block', element).append(replayButton); + + //add 80 confetti particles between 10% and 90% left values on the screen + for (let i=10; i<=90/*5*Math.floor(Math.random())+25*/; i++) { + let type = Math.floor(9*Math.random()); + let left = i; + let duration = 2*Math.random()+1.5; + switch(type) { + case 0: { + var confetti = "
"; + break; + }; + case 1: { + var confetti = "
"; + break; + }; + case 2: { + var confetti = "
"; + break; + }; + case 3: { + var confetti = "
"; + break; + }; + case 4: { + var confetti = "
"; + break; + }; + case 5: { + var confetti = "
"; + break; + }; + case 6: { + var confetti = "
"; + break; + }; + case 7: { + var confetti = "
"; + break; + }; + case 8: { + var confetti = "
"; + break; + }; + default: { + var confetti = "
"; + }; + } + $('.matching-end-block', element).append(confetti); + } + } + + //else record was not beaten; display current time and personal best (no need to worry about nulls since this will never be the first end page the user sees) + else { + var textBlock = "
"; + var congrats = "
Congratulations!
"; + var message = "
You completed the matching game in " + "" + formatTime(lastPage.new_time) + "" + "
Keep up the good work!
"; + var bestBlock = "
"; + var award = "
"; + var bestText = "
Your personal best
"; + var bestTime = "
" + formatTime(lastPage.prev_time) + "
"; + var replayButton = "
Play again
"; + + $('.matching-end-block', element).append(textBlock); + $('.matching-end-standard-text-block', element).append(congrats); + $('.matching-end-standard-text-block', element).append(message); + $('.matching-end-standard-text-block', element).append(bestBlock); + $('.matching-end-standard-best-block', element).append(award); + $('.matching-end-standard-best-block', element).append(bestText); + $('.matching-end-standard-best-block', element).append(bestTime); + $('.matching-end-standard-text-block', element).append(replayButton); + } + } + $(document).on('click', '.matching-container', function(eventObject) { + var containerID = $(this).attr("id"); + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'select_container'), + data: JSON.stringify({id: containerID}), + success: selectContainer + }); + }); + + //Reset Matching Game + function resetMatching(initialState) { + //Function to reset the matching game to its 'start screen' state + + $('.gamesxblock', element).empty(); + expandGame(initialState); + } + $(document).on('click', '.matching-replay-button', function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'expand_game'), + data: JSON.stringify({}), + success: resetMatching + }); + }); + + return {}; + + $(function ($) { + /* Here's where you'd do things on page load. */ + }); +} + + + + + + + +//Old stuff - no longer has a purpose. Will delete once editor view is complete. +/* + ////////////////////////////////////////////////////////////// + //Timer Flip + function flipTimer(newTimer) { + $('.timer_bool .timer_flip', element).text(newTimer.timer); + } + + $('.timer_bool', element).click(function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'flip_timer'), + data: JSON.stringify({}), + success: flipTimer + }); + }); + ////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////// + //Shuffle Flip + function flipShuffle(newShuffle) { + $('.shuffle_bool .shuffle_flip', element).text(newShuffle.shuffle); + } + + $('.shuffle_bool', element).click(function(eventObject) { + $.ajax({ + type: "POST", + url: runtime.handlerUrl(element, 'flip_shuffle'), + data: JSON.stringify({}), + success: flipShuffle + }); + }); + ////////////////////////////////////////////////////////////// +*/ \ No newline at end of file diff --git a/games/translations/README.txt b/games/translations/README.txt new file mode 100644 index 0000000..0493bcc --- /dev/null +++ b/games/translations/README.txt @@ -0,0 +1,4 @@ +Use this translations directory to provide internationalized strings for your XBlock project. + +For more information on how to enable translations, visit the Open edX XBlock tutorial on Internationalization: +http://edx.readthedocs.org/projects/xblock-tutorial/en/latest/edx_platform/edx_lms.html diff --git a/games_xblock.egg-info/PKG-INFO b/games_xblock.egg-info/PKG-INFO new file mode 100644 index 0000000..88e662c --- /dev/null +++ b/games_xblock.egg-info/PKG-INFO @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: games-xblock +Version: 0.1 +Summary: games XBlock +License: UNKNOWN +License-File: LICENSE diff --git a/games_xblock.egg-info/SOURCES.txt b/games_xblock.egg-info/SOURCES.txt new file mode 100644 index 0000000..5d85cd5 --- /dev/null +++ b/games_xblock.egg-info/SOURCES.txt @@ -0,0 +1,18 @@ +LICENSE +README.md +setup.py +games/__init__.py +games/games.py +games/static/README.txt +games/static/css/games.css +games/static/html/games.html +games/static/img/close_button.png +games/static/img/navigate_left.png +games/static/img/navigate_right.png +games/static/js/src/games.js +games_xblock.egg-info/PKG-INFO +games_xblock.egg-info/SOURCES.txt +games_xblock.egg-info/dependency_links.txt +games_xblock.egg-info/entry_points.txt +games_xblock.egg-info/requires.txt +games_xblock.egg-info/top_level.txt \ No newline at end of file diff --git a/games_xblock.egg-info/dependency_links.txt b/games_xblock.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/games_xblock.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/games_xblock.egg-info/entry_points.txt b/games_xblock.egg-info/entry_points.txt new file mode 100644 index 0000000..b5da055 --- /dev/null +++ b/games_xblock.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[xblock.v1] +games = games:GamesXBlock diff --git a/games_xblock.egg-info/requires.txt b/games_xblock.egg-info/requires.txt new file mode 100644 index 0000000..f681200 --- /dev/null +++ b/games_xblock.egg-info/requires.txt @@ -0,0 +1 @@ +XBlock diff --git a/games_xblock.egg-info/top_level.txt b/games_xblock.egg-info/top_level.txt new file mode 100644 index 0000000..84d4140 --- /dev/null +++ b/games_xblock.egg-info/top_level.txt @@ -0,0 +1 @@ +games diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d9269a3 --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +"""Setup for games XBlock.""" + + +import os + +from setuptools import setup + + +def package_data(pkg, roots): + """Generic function to find package_data. + + All of the files under each of the `roots` will be declared as package + data for package `pkg`. + + """ + data = [] + for root in roots: + for dirname, _, files in os.walk(os.path.join(pkg, root)): + for fname in files: + data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) + + return {pkg: data} + + +setup( + name='games-xblock', + version='0.1', + description='games XBlock', # TODO: write a better description. + license='UNKNOWN', # TODO: choose a license: 'AGPL v3' and 'Apache 2.0' are popular. + packages=[ + 'games', + ], + install_requires=[ + 'XBlock', + ], + entry_points={ + 'xblock.v1': [ + 'games = games:GamesXBlock', + ] + }, + package_data=package_data("games", ["static", "public"]), +)