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"]),
+)