diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 34c1f465c566..beea9827d12b 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -56,6 +56,7 @@
'html',
'video',
'problem',
+ 'game',
'itembank',
'library_v2', # Not an XBlock
'library',
diff --git a/lms/templates/game.html b/lms/templates/game.html
new file mode 100644
index 000000000000..21a27a0e08db
--- /dev/null
+++ b/lms/templates/game.html
@@ -0,0 +1,12 @@
+<%page expression_filter="h"/>
+
+
+ % if game_type:
+
+ % endif
+
+ ${data | n}
+
+
diff --git a/setup.py b/setup.py
index 5b9f020ac3c5..de897e2ea783 100644
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,7 @@
"discuss = xmodule.template_block:TranslateCustomTagBlock",
"discussion = xmodule.discussion_block:DiscussionXBlock",
"error = xmodule.error_block:ErrorBlock",
+ "game = xmodule.game_block:GameBlock",
"hidden = xmodule.hidden_block:HiddenBlock",
"html = xmodule.html_block:HtmlBlock",
"itembank = xmodule.item_bank_block:ItemBankBlock",
diff --git a/xmodule/game_block.py b/xmodule/game_block.py
new file mode 100644
index 000000000000..3f1895f1b24f
--- /dev/null
+++ b/xmodule/game_block.py
@@ -0,0 +1,75 @@
+# lint-amnesty, pylint: disable=missing-module-docstring
+
+import logging
+
+from web_fragments.fragment import Fragment
+from xblock.core import XBlock
+from xblock.fields import Scope, String
+
+from xmodule.x_module import (
+ XModuleMixin,
+ XModuleToXBlockMixin,
+)
+from xmodule.xml_block import XmlMixin
+
+log = logging.getLogger(__name__)
+
+# Make '_' a no-op so we can scrape strings
+_ = lambda text: text
+
+
+@XBlock.needs("mako")
+class GameBlock( # lint-amnesty, pylint: disable=abstract-method
+ XmlMixin,
+ XModuleToXBlockMixin,
+ XModuleMixin,
+ XBlock,
+):
+ """
+ Game XBlock for displaying interactive game content in courses.
+ This is a read-only component for students to view game content.
+ """
+
+ display_name = String(
+ display_name=_("Display Name"),
+ help=_("The display name for this component."),
+ scope=Scope.settings,
+ default=_("Game")
+ )
+
+ game_type = String(
+ help=_("Type of game"),
+ display_name=_("Game Type"),
+ default="",
+ scope=Scope.settings
+ )
+
+ data = String(
+ help=_("Game content to display for this block"),
+ default="",
+ scope=Scope.content
+ )
+
+ @XBlock.supports("multi_device")
+ def student_view(self, _context):
+ """
+ Return a fragment that contains the html for the student view
+ """
+ from xmodule.util.builtin_assets import add_css_to_fragment
+
+ context = {
+ 'game_type': self.game_type,
+ 'data': self.data if self.data else ""
+ }
+ fragment = Fragment(
+ self.runtime.service(self, 'mako').render_lms_template('game.html', context)
+ )
+ add_css_to_fragment(fragment, 'GameBlockDisplay.css')
+ return fragment
+
+ @XBlock.supports("multi_device")
+ def public_view(self, context):
+ """
+ Returns a fragment that contains the html for the preview view
+ """
+ return self.student_view(context)
diff --git a/xmodule/static/css-builtin-blocks/GameBlockDisplay.css b/xmodule/static/css-builtin-blocks/GameBlockDisplay.css
new file mode 100644
index 000000000000..6fb7500f17d7
--- /dev/null
+++ b/xmodule/static/css-builtin-blocks/GameBlockDisplay.css
@@ -0,0 +1,21 @@
+/* Game XBlock Display Styles */
+
+.game-xblock {
+ margin: 20px 0;
+}
+
+.game-xblock .game-type-header {
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--gray-l3, #c8c8c8);
+}
+
+.game-xblock .game-type-header strong {
+ font-size: 16px;
+ color: var(--body-color, #313131);
+}
+
+.game-xblock .game-content {
+ font-size: 14px;
+ color: var(--body-color, #313131);
+}