Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/games/snake/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@
"webpack-dev-server": "5.0.2"
},
"dependencies": {
"@eduplay/sdk-core": "file:/home/lloydho/eduplay/sdk/core",
"@eduplay/sdk-web": "file:/home/lloydho/eduplay/sdk/web",
"phaser": "3.80.1"
},
"resolutions": {
"@eduplay/sdk-core": "file:/home/lloydho/eduplay/sdk/core"
},
"repository": {
"type": "git",
"url": "git+https://github.com/digitsensitive/phaser3-typescript.git"
Expand Down
62 changes: 62 additions & 0 deletions src/games/snake/src/eduplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// === EduPlay integration ===
// Generated by /eduplay. See https://app.eduplay.com/docs/getting-started
//
// This is the mock-mode evaluation wiring. To ship for real:
// 1. Register a game at https://app.eduplay.com to get a real placement ID.
// 2. Remove `mock: true` and replace `placementId` with the real value.
// 3. Wire `studentToken` from your parent-portal-issued session URL param.

import { EduPlayWebClient, type EduPlayWebAd } from '@eduplay/sdk-web';

const client = EduPlayWebClient.initialize({
apiUrl: 'https://api.eduplay.com',
placementId: 'pl_mock00000000000000000000',
studentToken: new URLSearchParams(location.search).get('token') ?? '',
// Mock mode tells the hosted /break iframe to serve bundled mock questions
// instead of hitting the live API. No real placement / subscription needed
// for the integration to render end-to-end during evaluation.
mock: true,
});

client.start();

let pendingAd: EduPlayWebAd | null = null;

function preloadNextAd(): void {
client.loadAd({
onAdLoaded: (ad) => {
pendingAd = ad;
},
onAdFailedToLoad: (err) => {
// eslint-disable-next-line no-console
console.warn('[EduPlay] load failed', err);
},
});
}

preloadNextAd();

// Call this at natural pause points (level complete, lives lost, game over).
// `onReward` fires only on a correct answer; `onResume` always fires after
// the ad dismisses, so put your "return to game" logic there.
// Returns `true` if an ad was actually shown; `false` if none was preloaded
// (the caller should resume the game immediately in that case so it doesn't
// hang waiting for an ad that will never appear).
export function showAdBreak(opts?: {
onReward?: () => void;
onResume?: () => void;
}): boolean {
if (!pendingAd || !pendingAd.isLoaded()) return false;
pendingAd.show({
onUserEarnedReward: (reward) => {
if (reward.amount === 1) opts?.onReward?.();
},
onAdDismissed: () => {
pendingAd = null;
opts?.onResume?.();
preloadNextAd();
},
});
return true;
}
// === end EduPlay integration ===
1 change: 1 addition & 0 deletions src/games/snake/src/game.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'phaser';
import './eduplay'; // initializes EduPlay client + preloads the first question
import { GameConfig } from './config';

export class Game extends Phaser.Game {
Expand Down
21 changes: 19 additions & 2 deletions src/games/snake/src/scenes/game-scene.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Apple } from '../objects/apple';
import { Snake } from '../objects/snake';
import { CONST } from '../const/const';
import { showAdBreak } from '../eduplay';

export class GameScene extends Phaser.Scene {
// field and game setting
Expand All @@ -21,6 +22,10 @@ export class GameScene extends Phaser.Scene {
// texts
private scoreText: Phaser.GameObjects.BitmapText;

// EduPlay break gating — prevents re-firing showAdBreak() every frame while
// the player sits dead before the scene transitions back to the menu.
private adBreakTriggered: boolean;

constructor() {
super({
key: 'GameScene'
Expand All @@ -35,6 +40,7 @@ export class GameScene extends Phaser.Scene {
this.horizontalFields = this.boardWidth / CONST.FIELD_SIZE;
this.verticalFields = this.boardHeight / CONST.FIELD_SIZE;
this.tick = 0;
this.adBreakTriggered = false;
}

create(): void {
Expand Down Expand Up @@ -96,8 +102,19 @@ export class GameScene extends Phaser.Scene {
this.tick = time;
}
this.player.handleInput();
} else {
this.scene.start('MainMenuScene');
} else if (!this.adBreakTriggered) {
// EduPlay break-point: show an educational question at the natural
// game-over pause. onResume returns to the main menu (same behaviour
// as the original bare scene.start). If no ad is preloaded yet (rare
// — preloadNextAd is called at boot) fall back to the original flow
// so the player isn't stranded on the death screen.
this.adBreakTriggered = true;
const shown = showAdBreak({
onResume: () => this.scene.start('MainMenuScene'),
});
if (!shown) {
this.scene.start('MainMenuScene');
}
}
}

Expand Down
28 changes: 26 additions & 2 deletions src/games/snake/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==

"@eduplay/sdk-core@^0.1.0", "@eduplay/sdk-core@file:../../../../../eduplay/sdk/core", "@eduplay/sdk-core@file:/home/lloydho/eduplay/sdk/core":
version "0.1.0"

"@eduplay/sdk-web@file:../../../../../eduplay/sdk/web":
version "0.1.0"
dependencies:
"@eduplay/sdk-core" "^0.1.0"

"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
Expand Down Expand Up @@ -2401,7 +2409,16 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==

"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -2433,7 +2450,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down