{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "36cee816",
   "metadata": {},
   "source": [
    "# Řešení 3. série"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "efe16561",
   "metadata": {},
   "outputs": [],
   "source": [
    "from manim import *"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36ec08cc",
   "metadata": {},
   "source": [
    "## Simulace binomického rozložení "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e7a3da39",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%manim -v WARNING -qh --disable_caching BinomialDistributionSimulation\n",
    "\n",
    "from random import choice, seed\n",
    "\n",
    "\n",
    "class MoveAndFade(Animation):\n",
    "    def __init__(self, mobject: Mobject, path: VMobject, **kwargs):\n",
    "        self.path = path\n",
    "        self.original = mobject.copy()\n",
    "        super().__init__(mobject, **kwargs)\n",
    "\n",
    "    def interpolate_mobject(self, alpha: float) -> None:\n",
    "        point = self.path.point_from_proportion(self.rate_func(alpha))\n",
    "\n",
    "        # tohle není úplně čisté, jelikož pokaždé vytváříme nový objekt\n",
    "        # je to kvůli tomu, že obj.fade() nenastavuje průhlednost ale přidává jí\n",
    "        self.mobject.become(self.original.copy()).move_to(point).fade(alpha)\n",
    "\n",
    "\n",
    "class BinomialDistributionSimulation(Scene):\n",
    "    def construct(self):\n",
    "        seed(0xDEADBEEF2)  # hezčí vstupy :)\n",
    "\n",
    "        radius = 0.13\n",
    "        x_spacing = radius * 1.5\n",
    "        y_spacing = 4 * radius\n",
    "\n",
    "        n = 9\n",
    "        pyramid = VGroup()\n",
    "        pyramid_values = []\n",
    "\n",
    "        for i in range(1, n + 1):\n",
    "            row = VGroup()\n",
    "\n",
    "            for j in range(i):\n",
    "                obj = Dot()\n",
    "\n",
    "                # if it's the last row, make the rows numbers instead\n",
    "                if i == n:\n",
    "                    obj = Tex(\"0\")\n",
    "                    pyramid_values.append(0)\n",
    "\n",
    "                row.add(obj)\n",
    "\n",
    "            row.arrange(buff=2 * x_spacing)\n",
    "\n",
    "            if len(pyramid) != 0:\n",
    "                row.move_to(pyramid[-1]).shift(DOWN * y_spacing)\n",
    "\n",
    "            pyramid.add(row)\n",
    "\n",
    "        pyramid.move_to(RIGHT * 3.4)\n",
    "\n",
    "        x_values = np.arange(-n // 2 + 1, n // 2 + 1, 1)\n",
    "\n",
    "        def create_graph(x_values, y_values):\n",
    "            y_values_all = list(range(0, (max(y_values) or 1) + 1))\n",
    "\n",
    "            axes = (\n",
    "                Axes(\n",
    "                    x_range=[-n // 2 + 1, n // 2, 1],\n",
    "                    y_range=[0, max(y_values) or 1, 1],\n",
    "                    x_axis_config={\"numbers_to_include\": x_values},\n",
    "                    tips=False,\n",
    "                )\n",
    "                .scale(0.45)\n",
    "                .shift(LEFT * 3.0)\n",
    "            )\n",
    "\n",
    "            graph = axes.plot_line_graph(x_values=x_values, y_values=y_values)\n",
    "\n",
    "            return graph, axes\n",
    "\n",
    "        graph, axes = create_graph(x_values, pyramid_values)\n",
    "\n",
    "        self.play(Write(axes), Write(pyramid), Write(graph), run_time=1.5)\n",
    "\n",
    "        for iteration in range(120):\n",
    "            circle = (\n",
    "                Circle(fill_opacity=1, stroke_opacity=0)\n",
    "                .scale(radius)\n",
    "                .next_to(pyramid[0][0], UP, buff=0)\n",
    "            )\n",
    "\n",
    "            run_time = (\n",
    "                0.5\n",
    "                if iteration == 0\n",
    "                else 0.1\n",
    "                if iteration == 1\n",
    "                else 0.02\n",
    "                if iteration < 20\n",
    "                else 0.003\n",
    "            )\n",
    "\n",
    "            self.play(FadeIn(circle, shift=DOWN * 0.5), run_time=run_time * 2)\n",
    "\n",
    "            x = 0\n",
    "            for i in range(1, n):\n",
    "                next_position = choice([0, 1])\n",
    "                x += next_position\n",
    "\n",
    "                dir = LEFT if next_position == 0 else RIGHT\n",
    "\n",
    "                circle_center = circle.get_center()\n",
    "\n",
    "                # if it's not the last row, make the animation regular\n",
    "                if i != n - 1:\n",
    "                    b = CubicBezier(\n",
    "                        circle_center,\n",
    "                        circle_center + dir * x_spacing,\n",
    "                        circle_center + dir * x_spacing + DOWN * y_spacing / 2,\n",
    "                        circle.copy().next_to(pyramid[i][x], UP, buff=0).get_center(),\n",
    "                    )\n",
    "\n",
    "                    self.play(\n",
    "                        MoveAlongPath(circle, b, rate_func=rate_functions.ease_in_quad),\n",
    "                        run_time=run_time,\n",
    "                    )\n",
    "\n",
    "                # if it is, fade the circle and increment the number\n",
    "                else:\n",
    "                    b = CubicBezier(\n",
    "                        circle_center,\n",
    "                        circle_center + dir * x_spacing,\n",
    "                        circle_center + dir * x_spacing + DOWN * y_spacing / 2,\n",
    "                        pyramid[i][x].get_center(),\n",
    "                    )\n",
    "\n",
    "                    pyramid_values[x] += 1\n",
    "\n",
    "                    n_graph, n_axes = create_graph(x_values, pyramid_values)\n",
    "\n",
    "                    self.play(\n",
    "                        AnimationGroup(\n",
    "                            AnimationGroup(\n",
    "                                MoveAndFade(\n",
    "                                    circle, b, rate_func=rate_functions.ease_in_quad\n",
    "                                ),\n",
    "                                run_time=run_time,\n",
    "                            ),\n",
    "                            AnimationGroup(\n",
    "                                pyramid[i][x]\n",
    "                                .animate(run_time=run_time)\n",
    "                                .become(\n",
    "                                    Tex(str(pyramid_values[x])).move_to(pyramid[i][x])\n",
    "                                ),\n",
    "                                graph.animate.become(n_graph),\n",
    "                                axes.animate.become(n_axes),\n",
    "                                run_time=run_time,\n",
    "                            ),\n",
    "                            lag_ratio=0.3,\n",
    "                        )\n",
    "                    )\n",
    "\n",
    "        self.play(FadeOut(axes), FadeOut(pyramid), FadeOut(graph), run_time=1)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6f13b54",
   "metadata": {},
   "source": [
    "## 3D Game of Life"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc2941bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from manim import *\n",
    "from random import random, seed\n",
    "from enum import Enum\n",
    "\n",
    "# https://softologyblog.wordpress.com/2019/12/28/3d-cellular-automata-3/\n",
    "\n",
    "class Grid:\n",
    "    class ColorType(Enum):\n",
    "        FROM_COORDINATES = 0\n",
    "        FROM_PALETTE = 1\n",
    "\n",
    "    def __init__(self, scene, grid_size, survives_when, revives_when, state_count=2, size=1, palette=[\"#000b5e\", \"#001eff\"], color_type=ColorType.FROM_PALETTE):\n",
    "        self.grid = {}\n",
    "        self.scene = scene\n",
    "        self.grid_size = grid_size\n",
    "        self.size = size\n",
    "        self.survives_when = survives_when\n",
    "        self.revives_when = revives_when\n",
    "        self.state_count = state_count\n",
    "        self.palette = palette\n",
    "        self.color_type = color_type\n",
    "\n",
    "        self.bounding_box = Cube(side_length = self.size, color=GRAY, fill_opacity=0.05)\n",
    "        self.scene.add(self.bounding_box)\n",
    "\n",
    "    def fadeOut(self):\n",
    "        self.scene.play(\n",
    "                FadeOut(self.bounding_box),\n",
    "                *[FadeOut(self.grid[index][0]) for index in self.grid],\n",
    "                )\n",
    "\n",
    "    def __index_to_position(self, index):\n",
    "        \"\"\"Convert the index of a cell to its position in 3D.\"\"\"\n",
    "        dirs = [RIGHT, UP, OUT]\n",
    "\n",
    "        # pozor!\n",
    "        # nemůžeme k originu jen tak přičítat, jelikož to nevytváří nové objekty\n",
    "        # tím pádem bychom pořád měnili ten samý objekt, což nechceme -- proto list()\n",
    "        result = list(ORIGIN)\n",
    "        for dir, value in zip(dirs, index):\n",
    "            result += ((value - (self.grid_size- 1) / 2) /\n",
    "                       self.grid_size) * dir * self.size\n",
    "\n",
    "        return result\n",
    "\n",
    "    def __get_new_cell(self, index):\n",
    "        \"\"\"Create a new cell\"\"\"\n",
    "        cell = (Cube(side_length=1/self.grid_size * self.size, color=BLUE, fill_opacity=1).move_to(\n",
    "            self.__index_to_position(index)\n",
    "        ), self.state_count - 1)\n",
    "\n",
    "        self.__update_cell_color(index, *cell)\n",
    "\n",
    "        return cell\n",
    "\n",
    "    def __return_neighbouring_cell_coordinates(self, index):\n",
    "        \"\"\"Return the coordinates of the neighbourhood of a given index.\"\"\"\n",
    "        neighbourhood = set()\n",
    "        for dx in range(-1, 1 + 1):\n",
    "            for dy in range(-1, 1 + 1):\n",
    "                for dz in range(-1, 1 + 1):\n",
    "                    if dx == 0 and dy == 0 and dz == 0:\n",
    "                        continue\n",
    "\n",
    "                    nx = index[0] + dx\n",
    "                    ny = index[1] + dy\n",
    "                    nz = index[2] + dz\n",
    "\n",
    "                    if nx < 0 or nx >= self.grid_size or ny < 0 or ny >= self.grid_size or nz < 0 or nz >= self.grid_size:\n",
    "                        continue\n",
    "\n",
    "                    neighbourhood.add((nx, ny, nz))\n",
    "\n",
    "        return neighbourhood\n",
    "\n",
    "    def __count_neighbours(self, index):\n",
    "        \"\"\"Return the number of neighbouring cells for a given index (excluding itself).\"\"\"\n",
    "        total = 0\n",
    "        for neighbour_index in self.__return_neighbouring_cell_coordinates(index):\n",
    "            if neighbour_index in self.grid:\n",
    "                total += 1\n",
    "\n",
    "        return total\n",
    "\n",
    "    def __return_possible_cell_change_indexes(self):\n",
    "        \"\"\"Return the indexes of all possible cells that could change.\"\"\"\n",
    "        changes = set()\n",
    "        for index in self.grid:\n",
    "            changes |= self.__return_neighbouring_cell_coordinates(index).union({\n",
    "                index})\n",
    "        return changes\n",
    "\n",
    "    def toggle(self, index):\n",
    "        \"\"\"Toggle a given cell.\"\"\"\n",
    "        if index in self.grid:\n",
    "            self.scene.remove(self.grid[index][0])\n",
    "            del self.grid[index]\n",
    "        else:\n",
    "            self.grid[index] = self.__get_new_cell(index)\n",
    "            self.scene.add(self.grid[index][0])\n",
    "\n",
    "    def __update_cell_color(self, index, cell, age):\n",
    "        if self.color_type == self.ColorType.FROM_PALETTE:\n",
    "            state_colors = color_gradient(self.palette, self.state_count - 1)\n",
    "\n",
    "            cell.set_color(state_colors[age - 1])\n",
    "        else:\n",
    "            def coordToHex(n):\n",
    "                return hex(int(n * (256 / self.grid_size)))[2:].ljust(2, \"0\")\n",
    "\n",
    "            cell.set_color(f\"#{coordToHex(index[0])}{coordToHex(index[1])}{coordToHex(index[2])}\")\n",
    "\n",
    "    def do_iteration(self):\n",
    "        new_grid = {}\n",
    "        something_changed = False\n",
    "\n",
    "        for index in self.__return_possible_cell_change_indexes():\n",
    "            neighbours = self.__count_neighbours(index)\n",
    "\n",
    "            # alive rules\n",
    "            if index in self.grid:\n",
    "                cell, age = self.grid[index]\n",
    "\n",
    "                # always decrease age\n",
    "                if age != 1:\n",
    "                    age -= 1\n",
    "                    something_changed = True\n",
    "\n",
    "                # survive if within range or age isn't 1\n",
    "                if neighbours in self.survives_when or age != 1:\n",
    "                    self.__update_cell_color(index, cell, age)\n",
    "                    new_grid[index] = (cell, age)\n",
    "                else:\n",
    "                    self.scene.remove(self.grid[index][0])\n",
    "                    something_changed = True\n",
    "\n",
    "            # dead rules\n",
    "            else:\n",
    "                # revive if within range\n",
    "                if neighbours in self.revives_when:\n",
    "                    new_grid[index] = self.__get_new_cell(index)\n",
    "                    self.scene.add(new_grid[index][0])\n",
    "                    something_changed = True\n",
    "\n",
    "        self.grid = new_grid\n",
    "\n",
    "        return something_changed\n",
    "\n",
    "class GOLFirst(ThreeDScene):\n",
    "    def construct(self):\n",
    "        seed(0xDEADBEEF)\n",
    "\n",
    "        self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)\n",
    "        self.begin_ambient_camera_rotation(rate=-0.20)\n",
    "\n",
    "        grid_size = 16\n",
    "        size=3.5\n",
    "        grid = Grid(self, grid_size, [4, 5], [5], state_count=2, size=size, color_type=Grid.ColorType.FROM_COORDINATES)\n",
    "\n",
    "        for i in range(grid_size):\n",
    "            for j in range(grid_size):\n",
    "                for k in range(grid_size):\n",
    "                    if random() < 0.2:\n",
    "                        grid.toggle((i, j, k))\n",
    "\n",
    "        grid.fadeIn()\n",
    "\n",
    "        self.wait(1)\n",
    "\n",
    "        for i in range(50):\n",
    "            something_changed = grid.do_iteration()\n",
    "\n",
    "            if not something_changed:\n",
    "                break\n",
    "\n",
    "            self.wait(0.2)\n",
    "\n",
    "        self.wait(2)\n",
    "\n",
    "        grid.fadeOut()\n",
    "\n",
    "\n",
    "class GOL(ThreeDScene):\n",
    "    def construct(self):\n",
    "        seed(0xDEADBEEF)\n",
    "\n",
    "        self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)\n",
    "        self.begin_ambient_camera_rotation(rate=0.15)\n",
    "\n",
    "        grid_size = 16\n",
    "        size=3.5\n",
    "\n",
    "        # decay grid = Grid(self, grid_size, [1, 4, 8, 11] + list(range(13, 26 + 1)), list(range(13, 26 + 1)), state_count=5, size=size, color_type=Grid.ColorType.FROM_PALETTE)\n",
    "        grid = Grid(self, grid_size, [2, 6, 9], [4, 6, 8, 9], state_count=10, size=size, color_type=Grid.ColorType.FROM_PALETTE)\n",
    "\n",
    "        for i in range(grid_size):\n",
    "            for j in range(grid_size):\n",
    "                for k in range(grid_size):\n",
    "                    if random() < 0.3:\n",
    "                        grid.toggle((i, j, k))\n",
    "\n",
    "        self.wait(2)\n",
    "\n",
    "        for i in range(70):\n",
    "            something_changed = grid.do_iteration()\n",
    "\n",
    "            if not something_changed:\n",
    "                break\n",
    "\n",
    "            self.wait(0.1)\n",
    "\n",
    "        self.wait(2)\n",
    "\n",
    "        grid.fadeOut()\n"
   ]
  }
 ],
 "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.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}