Skip to content

Commit

Permalink
3335 UI idea extend skulpt with pygame4skulpt (hedyorg#3395)
Browse files Browse the repository at this point in the history
Make it possible for Skulpt to execute Pygame (partially), by including PyGame4Skulpt as a external library for Skulpt.

**Description**

Things worthy of mentioning:

1. [PyGame4Skulpt ](https://github.com/Dustyposa/pygame4skulpt) is added to Skulpt
2. variable _hasPygame_ is used analog to the _hasTurtle_ variable, for levels that will need Pygame functionality in the future.
3. the function runPythonProgram is changed to accommodate _hasPygame_ . This way the library is only imported when necessary.
4.  the function runPythonProgram is 'exported', it is accessible via the DOM. This makes it a lot easier to test programs by being able to run real Python, especially when testing Pygame related code.
5. Same canvas is being used for Turtle and Pygame code

**How to test**

1. Open up any level and copy & paste the following Python code into the editor

```python
import math, random

width = 711
heigth = 300

print('Drawing Circles!')

def get_random_color():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def draw_circle():
    r = d / (2 * n)
    kx = width / (2 * n)
    ky = heigth / (2 * n)
    canvas.fill(pygame.Color(247, 250, 252, 255))
    for i in range(n):
        (cx, cy) = ((2 * i + 1) * kx, heigth - (2 * i + 1) * ky)
        pygame.draw.circle(canvas, get_random_color(), (round(cx), round(cy)), round(r), 2)
    pygame.display.update()


d = math.sqrt(width ** 2 + heigth ** 2)

n = 7
draw_circle()

end = False
while not end:
    event = pygame.event.wait()
    if event.type == pygame.QUIT:
        end = True
    elif event.type == pygame.MOUSEBUTTONDOWN:
        if event.button == 1:
            n += 1
            draw_circle()
        elif event.button == 3 and n > 1:
            n -= 1
            draw_circle()
    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_UP:
            n += 1
            draw_circle()
        elif event.key == pygame.K_DOWN and n > 1:
            n -= 1
            draw_circle()

pygame.quit()
```

2. Open up your browser console & execute the following JS
```javascript
let code = hedyApp.get_trimmed_code() 
hedyApp.runPythonProgram(code, false, true, false, false)
```

3. You should now be able to increase the circles with the **up arrow/right mouse click** and decrease with the **down arrow/left mouse click**, you can stop executing Pygame with the **escape button**.
![image](https://user-images.githubusercontent.com/48225550/194782278-e0660d95-419f-4f1a-b7e0-f6a0658e5676.png)
  • Loading branch information
ToniSkulj authored Oct 13, 2022
1 parent 32f49cd commit 13463b1
Show file tree
Hide file tree
Showing 16 changed files with 3,890 additions and 23 deletions.
166 changes: 162 additions & 4 deletions static/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ t.speed(3)
t.showturtle()
`;

const pygame_prefix =
`# coding=utf8
import pygame
pygame.init()
canvas = pygame.display.set_mode((711,300))
canvas.fill(pygame.Color(247, 250, 252, 255))
`;

const normal_prefix =
`# coding=utf8
import random, time
Expand Down Expand Up @@ -375,6 +383,7 @@ export function runit(level: string, lang: string, disabled_prompt: string, cb:
clearErrors(editor);
removeBulb();
console.log('Original program:\n', code);

$.ajax({
type: 'POST',
url: '/parse',
Expand Down Expand Up @@ -408,7 +417,7 @@ export function runit(level: string, lang: string, disabled_prompt: string, cb:
$('#runit').show();
return;
}
runPythonProgram(response.Code, response.has_turtle, response.has_sleep, response.Warning, cb).catch(function(err) {
runPythonProgram(response.Code, response.has_turtle, response.has_pygame, response.has_sleep, response.Warning, cb).catch(function(err) {
// The err is null if we don't understand it -> don't show anything
if (err != null) {
error.show(ErrorMessages['Execute_error'], err.message);
Expand Down Expand Up @@ -918,7 +927,7 @@ window.onerror = function reportClientException(message, source, line_number, co
});
}

function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasSleep: boolean, hasWarnings: boolean, cb: () => void) {
export function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasPygame: boolean, hasSleep: boolean, hasWarnings: boolean, cb: () => void) {
// If we are in the Parsons problem -> use a different output
let outputDiv = $('#output');

Expand All @@ -930,12 +939,13 @@ function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasSleep:
outputDiv.append(variables);

const storage = window.localStorage;
let skulptExternalLibraries:{[index: string]:any} = {};
let debug = storage.getItem("debugLine");

Sk.pre = "output";
const turtleConfig = (Sk.TurtleGraphics || (Sk.TurtleGraphics = {}));
turtleConfig.target = 'turtlecanvas';
// If the adventures are not shown -> increase height of turtleConfig
// If the adventures are not shown -> increase height of turtleConfig
if ($('#adventures-tab').is(":hidden")) {
turtleConfig.height = 600;
turtleConfig.worldHeight = 600;
Expand All @@ -947,17 +957,66 @@ function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasSleep:
turtleConfig.width = outputDiv.width();
turtleConfig.worldWidth = outputDiv.width();

if (!hasTurtle) {
if (!hasTurtle && !hasPygame) {
// There might still be a visible turtle panel. If the new program does not use the Turtle,
// remove it (by clearing the '#turtlecanvas' div)
$('#turtlecanvas').empty();
code = normal_prefix + code
} else {
// Otherwise make sure that it is shown as it might be hidden from a previous code execution.
$('#turtlecanvas').show();
}

if (hasTurtle) {
code = normal_prefix + turtle_prefix + code
}

if (hasPygame){
skulptExternalLibraries = {
'./pygame.js': {
path: "/vendor/pygame_4_skulpt/init.js",
},
'./display.js': {
path: "/vendor/pygame_4_skulpt/display.js",
},
'./draw.js': {
path: "/vendor/pygame_4_skulpt/draw.js",
},
'./event.js': {
path: "/vendor/pygame_4_skulpt/event.js",
},
'./font.js': {
path: "/vendor/pygame_4_skulpt/font.js",
},
'./image.js': {
path: "/vendor/pygame_4_skulpt/image.js",
},
'./key.js': {
path: "/vendor/pygame_4_skulpt/key.js",
},
'./mouse.js': {
path: "/vendor/pygame_4_skulpt/mouse.js",
},
'./transform.js': {
path: "/vendor/pygame_4_skulpt/transform.js",
},
'./locals.js': {
path: "/vendor/pygame_4_skulpt/locals.js",
},
'src/lib/time.js': {
path: "/vendor/pygame_4_skulpt/time.js",
},
'./version.js': {
path: "/vendor/pygame_4_skulpt/version.js",
},
};

code = pygame_prefix + normal_prefix + code

initSkulpt4Pygame();
initCanvas4PyGame();
}

Sk.configure({
output: outf,
read: builtinRead,
Expand Down Expand Up @@ -1062,6 +1121,26 @@ function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasSleep:
}

function builtinRead(x: string) {
if (x in skulptExternalLibraries) {
const tmpPath = skulptExternalLibraries[x]["path"];
if (x === "./pygame.js") {
return Sk.misceval.promiseToSuspension(
fetch(tmpPath)
.then(r => r.text()))

} else {
let request = new XMLHttpRequest();
request.open("GET", tmpPath, false);
request.send();

if (request.status !== 200) {
return void 0
}

return request.responseText
}
}

if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
throw "File not found: '" + x + "'";
return Sk.builtinFiles["files"][x];
Expand Down Expand Up @@ -1120,6 +1199,85 @@ function runPythonProgram(this: any, code: string, hasTurtle: boolean, hasSleep:
}
}

function resetTurtleTarget() {
if (Sk.TurtleGraphics !== undefined) {

let selector = Sk.TurtleGraphics.target;
let target = typeof selector === "string" ? document.getElementById(selector) : selector;
if (target !== null && target !== undefined){
// clear canvas container
while (target.firstChild) {
target.removeChild(target.firstChild);
}
return target;
}

}

return null;
}

function initCanvas4PyGame() {
let currentTarget = resetTurtleTarget();

let div1 = document.createElement("div");

if (currentTarget !== null) {
currentTarget.appendChild(div1);
$(div1).addClass("modal");
$(div1).css("text-align", "center");

let div2 = document.createElement("div");
$(div2).addClass("modal-dialog modal-lg");
$(div2).css("display", "inline-block");
$(div2).width(self.width + 42);
$(div2).attr("role", "document");
div1.appendChild(div2);

let div3 = document.createElement("div");
$(div3).addClass("modal-content");
div2.appendChild(div3);

let div4 = document.createElement("div");
$(div4).addClass("modal-header d-flex justify-content-between");
let div5 = document.createElement("div");
$(div5).addClass("modal-body");
let div6 = document.createElement("div");
$(div6).addClass("modal-footer");
let div7 = document.createElement("div");
$(div7).addClass("col-md-8");
let div8 = document.createElement("div");
$(div8).addClass("col-md-4");

div3.appendChild(div4);
div3.appendChild(div5);
div3.appendChild(div6);

Sk.main_canvas.style.border = "none";
div5.appendChild(Sk.main_canvas);
}
}

function initSkulpt4Pygame() {
const killWhileAndForBool = true;
Sk.main_canvas = document.createElement("canvas");
Sk.builtin.KeyboardInterrupt = function (_:any) {
let o;
if (!(this instanceof Sk.builtin.KeyboardInterrupt)) {
o = Object.create(Sk.builtin.KeyboardInterrupt.prototype);
o.constructor.apply(o, arguments);
return o;
}
Sk.builtin.BaseException.apply(this, arguments);
};
Sk.abstr.setUpInheritance("KeyboardInterrupt", Sk.builtin.KeyboardInterrupt, Sk.builtin.BaseException);
Sk.configure({
killableWhile: killWhileAndForBool,
killableFor: killWhileAndForBool,
__future__: Sk.python3,
});
}

function speak(text: string) {
var selectedURI = $('#speak_dropdown').val();
if (!selectedURI) { return; }
Expand Down
39 changes: 22 additions & 17 deletions static/js/appbundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion static/js/hedy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ interface State {

declare interface Window {
State: State;

width: int;
editor?: AceAjax.Editor;
}

Expand Down
5 changes: 4 additions & 1 deletion static/js/vendor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ declare const Sk: {
execStart: date;
execLimit: number;
globals: Record<string, Variable>;
main_canvas: HTMLCanvasElement;
builtin;
abstr;

builtinFiles?: {
files: Record<string, string>;
Expand All @@ -24,7 +27,7 @@ declare const Sk: {

misceval: {
asyncToPromise<A>(fn: () => Suspension, handler?: Record<string, () => void>): Promise<A>;

promiseToSuspension;
Suspension: { }
},

Expand Down
52 changes: 52 additions & 0 deletions static/vendor/pygame_4_skulpt/display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
var $builtinmodule = function (name) {
var mod = {};
mod.init = new Sk.builtin.func(function () {
mod.__is_initialized = true;
return Sk.builtin.none.none$;
});
mod.quit = new Sk.builtin.func(function () {
mod.__is_initialized = false;
return Sk.builtin.none.none$;
});
mod.get_init = new Sk.builtin.func(function () {
if (mod.__is_initialized) {
return Sk.ffi.remapToPy(true);
}
return Sk.ffi.remapToPy(false);
});
mod.set_mode = new Sk.builtin.func(function (size, flags) {
var f = 0;
if (flags !== undefined) {
f = Sk.ffi.remapToJs(flags);
}
if (f & PygameLib.constants.FULLSCREEN) {
mod.surface = Sk.misceval.callsim(PygameLib.SurfaceType, size, true, true);
} else {
mod.surface = Sk.misceval.callsim(PygameLib.SurfaceType, size, false, true);
}
PygameLib.surface = mod.surface;
return mod.surface;
});
mod.get_surface = new Sk.builtin.func(function () {
return PygameLib.surface;
});
mod.update = new Sk.builtin.func(function () {
Sk.misceval.callsim(mod.surface.update, mod.surface);
});
mod.flip = new Sk.builtin.func(function () {
Sk.misceval.callsim(mod.surface.update, mod.surface);
});
mod.set_caption = new Sk.builtin.func(function (caption) {
PygameLib.caption = Sk.ffi.remapToJs(caption);
if (Sk.title_container) {
Sk.title_container.innerText = PygameLib.caption;
}
});
mod.get_caption = new Sk.builtin.func(function () {
return Sk.ffi.remapToPy(PygameLib.caption);
});
mod.get_active = new Sk.builtin.func(function () {
return Sk.ffi.remapToPy(document.hasFocus());
});
return mod;
};
Loading

0 comments on commit 13463b1

Please sign in to comment.