{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys\n", "if \"pyodide\" in sys.modules:\n", " import piplite\n", " await piplite.install('pyb2d-jupyterlite-backend>=0.4.2')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy\n", "import b2d\n", "import math\n", "import random\n", "\n", "from b2d.testbed import TestbedBase\n", "\n", "class Billiard(TestbedBase):\n", "\n", " name = \"Billiard\"\n", "\n", " def __init__(self, settings=None):\n", " super(Billiard, self).__init__(gravity=(0, 0), settings=settings)\n", " dimensions = [30, 50]\n", " self.dimensions = dimensions\n", "\n", " # the outer box\n", " box_shape = b2d.ChainShape()\n", " box_shape.create_loop(\n", " [\n", " (0, 0),\n", " (0, dimensions[1]),\n", " (dimensions[0], dimensions[1]),\n", " (dimensions[0], 0),\n", " ]\n", " )\n", " self.ball_radius = 1\n", " box = self.world.create_static_body(\n", " position=(0, 0), fixtures=b2d.fixture_def(shape=box_shape, friction=0)\n", " )\n", "\n", " self.place_balls()\n", " self.place_pockets()\n", "\n", " # mouse interaction\n", " self._selected_ball = None\n", " self._selected_ball_pos = None\n", " self._last_pos = None\n", "\n", " # balls to be destroyed in the next step\n", " # since they are in the pocket\n", " self._to_be_destroyed = []\n", "\n", " def place_pockets(self):\n", " pocket_radius = 1\n", " self.pockets = []\n", "\n", " def place_pocket(position):\n", " pocket_shape = b2d.circle_shape(radius=pocket_radius / 3)\n", " pocket = self.world.create_static_body(\n", " position=position,\n", " fixtures=b2d.fixture_def(shape=pocket_shape, is_sensor=True),\n", " user_data=(\"pocket\", None),\n", " )\n", " self.pockets.append(pocket)\n", "\n", " d = pocket_radius / 2\n", "\n", " place_pocket(position=(0 + d, 0 + d))\n", " place_pocket(position=(self.dimensions[0] - d, 0 + d))\n", "\n", " place_pocket(position=(0 + d, self.dimensions[1] / 2))\n", " place_pocket(position=(self.dimensions[0] - d, self.dimensions[1] / 2))\n", "\n", " place_pocket(position=(0 + d, self.dimensions[1] - d))\n", " place_pocket(position=(self.dimensions[0] - d, self.dimensions[1] - d))\n", "\n", " def place_balls(self):\n", " self.balls = []\n", "\n", " base_colors = [\n", " (1, 1, 0),\n", " (0, 0, 1),\n", " (1, 0, 0),\n", " (1, 0, 1),\n", " (1, 0.6, 0),\n", " (0, 1, 0),\n", " (0.7, 0.4, 0.4),\n", " ]\n", " colors = []\n", " for color in base_colors:\n", " # ``full`` ball\n", " colors.append((color, color))\n", " # ``half`` ball (half white)\n", " colors.append((color, (1, 1, 1)))\n", "\n", " random.shuffle(colors)\n", " colors.insert(4, ((0, 0, 0), (0, 0, 0))) # black\n", "\n", " n_y = 5\n", " c_x = self.dimensions[0] / 2\n", " diameter = (self.ball_radius * 2) * 1.01\n", "\n", " bi = 0\n", " for y in range(n_y):\n", "\n", " py = y * diameter * 0.5 * math.sqrt(3)\n", " n_x = y + 1\n", " ox = diameter * (n_y - y) / 2\n", " for x in range(y + 1):\n", " position = (x * diameter + 10 + ox, py + 30)\n", " self.create_billard_ball(position=position, color=colors[bi])\n", " bi += 1\n", "\n", " self.create_billard_ball(position=(c_x, 10), color=((1, 1, 1), (1, 1, 1)))\n", "\n", " def create_billard_ball(self, position, color):\n", "\n", " ball = self.world.create_dynamic_body(\n", " position=position,\n", " fixtures=b2d.fixture_def(\n", " shape=b2d.circle_shape(radius=self.ball_radius),\n", " density=1.0,\n", " restitution=0.8,\n", " ),\n", " linear_damping=0.8,\n", " user_data=(\"ball\", color),\n", " fixed_rotation=True,\n", " )\n", " self.balls.append(ball)\n", "\n", " def begin_contact(self, contact):\n", " body_a = contact.body_a\n", " body_b = contact.body_b\n", "\n", " ud_a = body_a.user_data\n", " ud_b = body_b.user_data\n", " if ud_a is None or ud_b is None:\n", " return\n", "\n", " if ud_b[0] == \"ball\":\n", " body_a, body_b = body_b, body_a\n", " ud_a, ud_b = ud_b, ud_a\n", "\n", " if ud_a[0] == \"ball\" and ud_b[0] == \"pocket\":\n", " self._to_be_destroyed.append(body_a)\n", "\n", " def pre_step(self, dt):\n", " for b in self._to_be_destroyed:\n", " self.balls.remove(b)\n", " self.world.destroy_body(b)\n", " self._to_be_destroyed = []\n", "\n", " def ball_at_position(self, pos):\n", " body = self.world.find_body(pos)\n", " if body is not None:\n", " user_data = body.user_data\n", " if user_data is not None and user_data[0] == \"ball\":\n", " return body\n", " return None\n", "\n", " def on_mouse_down(self, pos):\n", " body = self.ball_at_position(pos)\n", " if body is not None:\n", " self._selected_ball = body\n", " self._selected_ball_pos = pos\n", " return True\n", "\n", " return False\n", "\n", " def on_mouse_move(self, pos):\n", " if self._selected_ball is not None:\n", " self._last_pos = pos\n", " return True\n", " return False\n", "\n", " def on_mouse_up(self, pos):\n", " if self._selected_ball is not None:\n", " self._last_pos = pos\n", " # if the mouse is in the starting ball itself we do nothing\n", " if self.ball_at_position(pos) != self._selected_ball:\n", " delta = b2d.vec2(self._selected_ball_pos) - b2d.vec2(self._last_pos)\n", " delta *= 100.0\n", " self._selected_ball.apply_linear_impulse(\n", " delta, self._selected_ball_pos, True\n", " )\n", " self._selected_ball = None\n", " self._selected_ball_pos = None\n", " self._last_pos = None\n", " return False\n", "\n", " def post_debug_draw(self):\n", "\n", " for pocket in self.pockets:\n", " self.debug_draw.draw_solid_circle(\n", " pocket.position, self.ball_radius, (1, 0), (1, 1, 1)\n", " )\n", "\n", " for ball in self.balls:\n", " _, (color0, color1) = ball.user_data\n", "\n", " self.debug_draw.draw_solid_circle(\n", " ball.position, self.ball_radius, (1, 0), color0\n", " )\n", " self.debug_draw.draw_solid_circle(\n", " ball.position, self.ball_radius / 2, (1, 0), color1\n", " )\n", " self.debug_draw.draw_circle(\n", " ball.position, self.ball_radius, (1, 1, 1), line_width=0.1\n", " )\n", "\n", " if self._selected_ball is not None:\n", "\n", " # draw circle around selected ball\n", " self.debug_draw.draw_circle(\n", " self._selected_ball.position,\n", " self.ball_radius * 2,\n", " (1, 1, 1),\n", " line_width=0.2,\n", " )\n", "\n", " # mark position on selected ball with red dot\n", " self.debug_draw.draw_solid_circle(\n", " self._selected_ball_pos, self.ball_radius * 0.2, (1, 0), (1, 0, 0)\n", " )\n", "\n", " # draw the line between marked pos on ball and last pos\n", " if self._last_pos is not None:\n", " self.debug_draw.draw_segment(\n", " self._selected_ball_pos, self._last_pos, (1, 1, 1), line_width=0.2\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Controlls\n", "* To play this game, click and hold inside a billiard ball, move and release the mouse to shoot the ball.\n", "* Use the mouse-wheel to zoom in/out, a\n", "* Click and drag in the empty space to translate the view." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pyb2d_jupyterlite_backend.async_jupyter_gui import JupyterAsyncGui\n", "backend = JupyterAsyncGui\n", "s = backend.Settings()\n", "s.resolution = [500,600]\n", "s.scale = 8\n", "s.fps = 40\n", "s.translate = [125,100]\n", "b2d.testbed.run(Billiard, backend=backend, gui_settings=s);" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.4" } }, "nbformat": 4, "nbformat_minor": 4 }