from big_ol_pile_of_manim_imports import *

class CountingScene(Scene):
    CONFIG = {
        "base" : 10,
        "power_colors" : [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D],
        "counting_dot_starting_position" : (FRAME_X_RADIUS-1)*RIGHT + (FRAME_Y_RADIUS-1)*UP,
        "count_dot_starting_radius" : 0.5,
        "dot_configuration_height" : 2,
        "ones_configuration_location" : UP+2*RIGHT,
        "num_scale_factor" : 2,
        "num_start_location" : 2*DOWN,
    }
    def setup(self):
        self.dots = VGroup()
        self.number = 0        
        self.number_mob = VGroup(TexMobject(str(self.number)))
        self.number_mob.scale(self.num_scale_factor)
        self.number_mob.shift(self.num_start_location)
        self.digit_width = self.number_mob.get_width()

        self.initialize_configurations()
        self.arrows = VGroup()
        self.add(self.number_mob)

    def get_template_configuration(self):
        #This should probably be replaced for non-base-10 counting scenes
        down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN
        result = []
        for down_right_steps in range(5):
            for left_steps in range(down_right_steps):
                result.append(
                    down_right_steps*down_right + left_steps*LEFT
                )
        return reversed(result[:self.base])

    def get_dot_template(self):
        #This should be replaced for non-base-10 counting scenes
        down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN
        dots = VGroup(*[
            Dot(
                point, 
                radius = 0.25,
                fill_opacity = 0,
                stroke_width = 2,
                stroke_color = WHITE,
            )
            for point in self.get_template_configuration()
        ])
        dots[-1].set_stroke(width = 0)
        dots.set_height(self.dot_configuration_height)
        return dots

    def initialize_configurations(self):
        self.dot_templates = []
        self.dot_template_iterators = []
        self.curr_configurations = []

    def add_configuration(self):
        new_template = self.get_dot_template()
        new_template.move_to(self.ones_configuration_location)
        left_vect = (new_template.get_width()+LARGE_BUFF)*LEFT
        new_template.shift(
            left_vect*len(self.dot_templates)
        )
        self.dot_templates.append(new_template)
        self.dot_template_iterators.append(
            it.cycle(new_template)
        )
        self.curr_configurations.append(VGroup())

    def count(self, max_val, run_time_per_anim = 1):
        for x in range(max_val):
            self.increment(run_time_per_anim)

    def increment(self, run_time_per_anim = 1, added_anims = [], total_run_time = None):
        run_all_at_once = (total_run_time is not None)
        if run_all_at_once:
            num_rollovers = self.get_num_rollovers()
            run_time_per_anim = float(total_run_time)/(num_rollovers+1)
        moving_dot = Dot(
            self.counting_dot_starting_position,
            radius = self.count_dot_starting_radius,
            color = self.power_colors[0],
        )
        moving_dot.generate_target()
        moving_dot.set_fill(opacity = 0)

        continue_rolling_over = True
        place = 0
        self.number += 1
        added_anims = list(added_anims) #Silly python objects...
        added_anims += self.get_new_configuration_animations()
        while continue_rolling_over:          
            moving_dot.target.replace(
                next(self.dot_template_iterators[place])
            )
            if run_all_at_once:
                denom = float(num_rollovers+1)
                start_t = place/denom
                def get_modified_rate_func(anim):
                    return lambda t : anim.original_rate_func(
                        start_t + t/denom
                    )
                for anim in added_anims:
                    if not hasattr(anim, "original_rate_func"):
                        anim.original_rate_func = anim.rate_func
                    anim.rate_func = get_modified_rate_func(anim)
            self.play(
                MoveToTarget(moving_dot), 
                *added_anims, 
                run_time = run_time_per_anim
            )
            self.curr_configurations[place].add(moving_dot)
            if not run_all_at_once:
                added_anims = []


            if len(self.curr_configurations[place].split()) == self.base:
                full_configuration = self.curr_configurations[place]
                self.curr_configurations[place] = VGroup()
                place += 1
                center = full_configuration.get_center_of_mass()
                radius = 0.6*max(
                    full_configuration.get_width(),
                    full_configuration.get_height(),
                )
                circle = Circle(
                    radius = radius,
                    stroke_width = 0,
                    fill_color = self.power_colors[place],
                    fill_opacity = 0.5,
                )
                circle.move_to(center)
                moving_dot = VGroup(circle, full_configuration)
                moving_dot.generate_target()
                moving_dot[0].set_fill(opacity = 0)
            else:
                continue_rolling_over = False
        self.play(*self.get_digit_increment_animations())

    def get_new_configuration_animations(self):
        if self.is_perfect_power():
            self.add_configuration()
            return [FadeIn(self.dot_templates[-1])]
        else:
            return []

    def get_digit_increment_animations(self):
        result = []
        new_number_mob = self.get_number_mob(self.number)
        new_number_mob.move_to(self.number_mob, RIGHT)
        if self.is_perfect_power():
            place = len(new_number_mob.split())-1
            arrow = Arrow(
                new_number_mob[place].get_top(),
                self.dot_templates[place].get_bottom(),
                color = self.power_colors[place]
            )
            self.arrows.add(arrow)
            result.append(ShowCreation(arrow))
        result.append(Transform(
            self.number_mob, new_number_mob,
            lag_ratio = 0.5
        ))
        return result

    def get_number_mob(self, num):
        result = VGroup()
        place = 0
        while num > 0:
            digit = TexMobject(str(num % self.base))
            if place >= len(self.power_colors):
                self.power_colors += self.power_colors
            digit.set_color(self.power_colors[place])
            digit.scale(self.num_scale_factor)
            digit.move_to(result, RIGHT)
            digit.shift(place*(self.digit_width+SMALL_BUFF)*LEFT)
            result.add(digit)
            num /= self.base
            place += 1
        return result

    def is_perfect_power(self):
        number = self.number
        while number > 1:
            if number%self.base != 0:
                return False
            number /= self.base
        return True

    def get_num_rollovers(self):
        next_number = self.number + 1
        result = 0
        while next_number%self.base == 0:
            result += 1
            next_number /= self.base
        return result

class BinaryCountingScene(CountingScene):
    CONFIG = {
        "base" : 2,
        "dot_configuration_height" : 1,
        "ones_configuration_location" : UP+5*RIGHT
    }
    def get_template_configuration(self):
        return [ORIGIN, UP]

class CountInDecimal(CountingScene):
    def construct(self):
        for x in range(11):
            self.increment()
        for x in range(85):
            self.increment(0.25)
        for x in range(20):
            self.increment()

class CountInTernary(CountingScene):
    CONFIG = {
        "base" : 3,
        "dot_configuration_height" : 1,
        "ones_configuration_location" : UP+4*RIGHT
    }
    def construct(self):
        self.count(27)

    # def get_template_configuration(self):
    #     return [ORIGIN, UP]

class CountTo27InTernary(CountInTernary):
    def construct(self):
        for x in range(27):
            self.increment()
        self.wait()

class CountInBinaryTo256(BinaryCountingScene):
    def construct(self):
        self.count(256, 0.25)

class TowersOfHanoiScene(Scene):
    CONFIG = {
        "disk_start_and_end_colors" : [BLUE_E, BLUE_A],
        "num_disks" : 5,
        "peg_width" : 0.25,
        "peg_height" : 2.5,
        "peg_spacing" : 4,
        "include_peg_labels" : True,
        "middle_peg_bottom" : 0.5*DOWN,
        "disk_height" : 0.4,
        "disk_min_width" : 1,
        "disk_max_width" : 3,
        "default_disk_run_time_off_peg" : 1,
        "default_disk_run_time_on_peg" : 2,
    }
    def setup(self):
        self.add_pegs()
        self.add_disks()

    def add_pegs(self):
        peg = Rectangle(
            height = self.peg_height,
            width = self.peg_width, 
            stroke_width = 0,
            fill_color = GREY_BROWN,
            fill_opacity = 1,
        )
        peg.move_to(self.middle_peg_bottom, DOWN)
        self.pegs = VGroup(*[
            peg.copy().shift(vect)
            for vect in (self.peg_spacing*LEFT, ORIGIN, self.peg_spacing*RIGHT)
        ])
        self.add(self.pegs)
        if self.include_peg_labels:
            self.peg_labels = VGroup(*[
                TexMobject(char).next_to(peg, DOWN)
                for char, peg in zip("ABC", self.pegs)
            ])
            self.add(self.peg_labels)

    def add_disks(self):
        self.disks = VGroup(*[
            Rectangle(
                height = self.disk_height,
                width = width,
                fill_color = color,
                fill_opacity = 0.8,
                stroke_width = 0,
            )
            for width, color in zip(
                np.linspace(
                    self.disk_min_width, 
                    self.disk_max_width,
                    self.num_disks
                ),
                color_gradient(
                    self.disk_start_and_end_colors,
                    self.num_disks
                )
            )
        ])
        for number, disk in enumerate(self.disks):
            label = TexMobject(str(number))
            label.set_color(BLACK)
            label.set_height(self.disk_height/2)
            label.move_to(disk)
            disk.add(label)
            disk.label = label
        self.reset_disks(run_time = 0)

        self.add(self.disks)

    def reset_disks(self, **kwargs):
        self.disks.generate_target()
        self.disks.target.arrange(DOWN, buff = 0)
        self.disks.target.move_to(self.pegs[0], DOWN)
        self.play(
            MoveToTarget(self.disks), 
            **kwargs
        )
        self.disk_tracker = [
            set(range(self.num_disks)),
            set([]),
            set([])
        ]

    def disk_index_to_peg_index(self, disk_index):
        for index, disk_set in enumerate(self.disk_tracker):
            if disk_index in disk_set:
                return index
        raise Exception("Somehow this disk wasn't accounted for...")

    def min_disk_index_on_peg(self, peg_index):
        disk_index_set = self.disk_tracker[peg_index]
        if disk_index_set:
            return min(self.disk_tracker[peg_index])
        else:
            return self.num_disks

    def bottom_point_for_next_disk(self, peg_index):
        min_disk_index = self.min_disk_index_on_peg(peg_index)
        if min_disk_index >= self.num_disks:
            return self.pegs[peg_index].get_bottom()
        else:
            return self.disks[min_disk_index].get_top()

    def get_next_disk_0_peg(self):
        curr_peg_index = self.disk_index_to_peg_index(0)
        return (curr_peg_index+1)%3

    def get_available_peg(self, disk_index):
        if disk_index == 0:
            return self.get_next_disk_0_peg()
        for index in range(len(list(self.pegs))):
            if self.min_disk_index_on_peg(index) > disk_index:
                return index
        raise Exception("Tower's of Honoi rule broken: No available disks")

    def set_disk_config(self, peg_indices):
        assert(len(peg_indices) == self.num_disks)
        self.disk_tracker = [set([]) for x in range(3)]
        for n, peg_index in enumerate(peg_indices):
            disk_index = self.num_disks - n - 1
            disk = self.disks[disk_index]
            peg = self.pegs[peg_index]
            disk.move_to(peg.get_bottom(), DOWN)
            n_disks_here = len(self.disk_tracker[peg_index])
            disk.shift(disk.get_height()*n_disks_here*UP)
            self.disk_tracker[peg_index].add(disk_index)

    def move_disk(self, disk_index, **kwargs):
        next_peg_index = self.get_available_peg(disk_index)
        self.move_disk_to_peg(disk_index, next_peg_index, **kwargs)

    def move_subtower_to_peg(self, num_disks, next_peg_index, **kwargs):
        disk_indices = list(range(num_disks))
        peg_indices = list(map(self.disk_index_to_peg_index, disk_indices))
        if len(set(peg_indices)) != 1:
            warnings.warn("These disks don't make up a tower right now")
        self.move_disks_to_peg(disk_indices, next_peg_index, **kwargs)

    def move_disk_to_peg(self, disk_index, next_peg_index, **kwargs):
        self.move_disks_to_peg([disk_index], next_peg_index, **kwargs)

    def move_disks_to_peg(self, disk_indices, next_peg_index, run_time = None, stay_on_peg = True, added_anims = []):
        if run_time is None:
            if stay_on_peg is True:
                run_time = self.default_disk_run_time_on_peg
            else:
                run_time = self.default_disk_run_time_off_peg
        disks = VGroup(*[self.disks[index] for index in disk_indices])
        max_disk_index = max(disk_indices)
        next_peg = self.pegs[next_peg_index]        
        curr_peg_index = self.disk_index_to_peg_index(max_disk_index)
        curr_peg = self.pegs[curr_peg_index]
        if self.min_disk_index_on_peg(curr_peg_index) != max_disk_index:
            warnings.warn("Tower's of Hanoi rule broken: disk has crap on top of it")
        target_bottom_point = self.bottom_point_for_next_disk(next_peg_index)
        path_arc = np.sign(curr_peg_index-next_peg_index)*np.pi/3
        if stay_on_peg:
            self.play(
                Succession(
                    ApplyMethod(disks.next_to, curr_peg, UP, 0),
                    ApplyMethod(disks.next_to, next_peg, UP, 0, path_arc = path_arc),
                    ApplyMethod(disks.move_to, target_bottom_point, DOWN),
                ),
                *added_anims,
                run_time = run_time,
                rate_func = lambda t : smooth(t, 2)
            )
        else:
            self.play(
                ApplyMethod(disks.move_to, target_bottom_point, DOWN),
                *added_anims,
                path_arc = path_arc*2,
                run_time = run_time,
                rate_func = lambda t : smooth(t, 2)
            )
        for disk_index in disk_indices:
            self.disk_tracker[curr_peg_index].remove(disk_index)
            self.disk_tracker[next_peg_index].add(disk_index)

class ConstrainedTowersOfHanoiScene(TowersOfHanoiScene):
    def get_next_disk_0_peg(self):
        if not hasattr(self, "total_disk_0_movements"):
            self.total_disk_0_movements = 0
        curr_peg_index = self.disk_index_to_peg_index(0)        
        if (self.total_disk_0_movements/2)%2 == 0:
            result = curr_peg_index + 1
        else:
            result = curr_peg_index - 1
        self.total_disk_0_movements += 1
        return result

def get_ruler_sequence(order = 4):
    if order == -1:
        return []
    else:
        smaller = get_ruler_sequence(order - 1)
        return smaller + [order] + smaller

def get_ternary_ruler_sequence(order = 4):
    if order == -1:
        return []
    else:
        smaller = get_ternary_ruler_sequence(order-1)
        return smaller+[order]+smaller+[order]+smaller

class SolveHanoi(TowersOfHanoiScene):
    def construct(self):
        self.wait()
        for x in get_ruler_sequence(self.num_disks-1):
            self.move_disk(x, stay_on_peg = False)
        self.wait()

class SolveConstrainedHanoi(ConstrainedTowersOfHanoiScene):
    def construct(self):
        self.wait()
        for x in get_ternary_ruler_sequence(self.num_disks-1):
            self.move_disk(x, run_time = 0.5, stay_on_peg = False)
        self.wait()

class Keith(PiCreature):
    CONFIG = {
        "color" : GREEN_D
    }
        
def get_binary_tex_mobs(num_list):
    result = VGroup()
    zero_width = TexMobject("0").get_width()
    nudge = zero_width + SMALL_BUFF
    for num in num_list:
        bin_string = bin(num)[2:]#Strip off the "0b" prefix
        bits = VGroup(*list(map(TexMobject, bin_string)))
        for n, bit in enumerate(bits):
            bit.shift(n*nudge*RIGHT)
        bits.move_to(ORIGIN, RIGHT)
        result.add(bits)
    return result

def get_base_b_tex_mob(number, base, n_digits):
    assert(number < base**n_digits)
    curr_digit = n_digits - 1
    zero = TexMobject("0")
    zero_width = zero.get_width()
    zero_height = zero.get_height()
    result = VGroup()
    for place in range(n_digits):
        remainder = number%base
        digit_mob = TexMobject(str(remainder))
        digit_mob.set_height(zero_height)
        digit_mob.shift(place*(zero_width+SMALL_BUFF)*LEFT)
        result.add(digit_mob)
        number = (number - remainder)/base
    return result.center()

def get_binary_tex_mob(number, n_bits = 4):
    return get_base_b_tex_mob(number, 2, n_bits)

def get_ternary_tex_mob(number, n_trits = 4):
    return get_base_b_tex_mob(number, 3, n_trits)


####################

class IntroduceKeith(Scene):
    def construct(self):
        morty = Mortimer(mode = "happy")
        keith = Keith(mode = "dance_kick")
        keith_image = ImageMobject("keith_schwarz", invert = False)
        # keith_image = Rectangle()
        keith_image.set_height(FRAME_HEIGHT - 2)
        keith_image.next_to(ORIGIN, LEFT)
        keith.move_to(keith_image, DOWN+RIGHT)
        morty.next_to(keith, buff = LARGE_BUFF, aligned_edge = DOWN)
        morty.make_eye_contact(keith)
        randy = Randolph().next_to(keith, LEFT, LARGE_BUFF, aligned_edge = DOWN)
        randy.shift_onto_screen()

        bubble = keith.get_bubble(SpeechBubble, width = 7)
        bubble.write("01101011 $\\Rightarrow$ Towers of Hanoi")
        zero_width = bubble.content[0].get_width()
        one_width = bubble.content[1].get_width()        
        for mob in bubble.content[:8]:
            if abs(mob.get_width() - zero_width) < 0.01:
                mob.set_color(GREEN)
            else:
                mob.set_color(YELLOW)

        bubble.resize_to_content()
        bubble.pin_to(keith)
        VGroup(bubble, bubble.content).shift(DOWN)

        randy.bubble = randy.get_bubble(SpeechBubble, height = 3)
        randy.bubble.write("Wait, what's \\\\ Towers of Hanoi?")

        title = TextMobject("Keith Schwarz (Computer scientist)")
        title.to_edge(UP)

        self.add(keith_image, morty)
        self.play(Write(title))
        self.play(FadeIn(keith, run_time = 2))
        self.play(FadeOut(keith_image), Animation(keith))
        self.play(Blink(morty))
        self.play(
            keith.change_mode, "speaking",
            keith.set_height, morty.get_height(),
            keith.next_to, morty, LEFT, LARGE_BUFF,
            run_time = 1.5
        )
        self.play(
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(
            morty.change_mode, "pondering",
            morty.look_at, bubble
        )
        self.play(Blink(keith))
        self.wait()
        original_content = bubble.content
        bubble.write("I'm usually meh \\\\ on puzzles")
        self.play(
            keith.change_mode, "hesitant",
            Transform(original_content, bubble.content),
        )
        self.play(
            morty.change_mode, "happy",
            morty.look_at, keith.eyes
        )
        self.play(Blink(keith))
        bubble.write("But \\emph{analyzing} puzzles!")
        VGroup(*bubble.content[3:12]).set_color(YELLOW)
        self.play(
            keith.change_mode, "hooray",
            Transform(original_content, bubble.content)
        )
        self.play(Blink(morty))
        self.wait()
        self.play(FadeIn(randy))
        self.play(
            randy.change_mode, "confused",
            randy.look_at, keith.eyes,
            keith.change_mode, "plain",
            keith.look_at, randy.eyes,
            morty.change_mode, "plain",
            morty.look_at, randy.eyes,
            FadeOut(bubble),
            FadeOut(original_content),
            ShowCreation(randy.bubble),
            Write(randy.bubble.content)
        )
        self.play(Blink(keith))
        self.play(
            keith.change_mode, "hooray",
            keith.look_at, randy.eyes
        )
        self.wait()

class IntroduceTowersOfHanoi(TowersOfHanoiScene):
    def construct(self):
        self.clear()
        self.add_title()
        self.show_setup()
        self.note_disk_labels()
        self.show_more_disk_possibility()
        self.move_full_tower()
        self.move_single_disk()
        self.cannot_move_disk_onto_smaller_disk()

    def add_title(self):
        title = TextMobject("Towers of Hanoi")
        title.to_edge(UP)
        self.add(title)
        self.title = title

    def show_setup(self):
        self.pegs.save_state()
        bottom = self.pegs.get_bottom()
        self.pegs.stretch_to_fit_height(0)
        self.pegs.move_to(bottom)
        self.play(
            ApplyMethod(
                self.pegs.restore, 
                lag_ratio = 0.5,
                run_time = 2
            ),
            Write(self.peg_labels)
        )
        self.wait()
        self.bring_in_disks()
        self.wait()

    def bring_in_disks(self):
        peg = self.pegs[0]
        disk_groups = VGroup()
        for disk in self.disks:
            top = Circle(radius = disk.get_width()/2)
            inner = Circle(radius = self.peg_width/2)
            inner.flip()
            top.add_subpath(inner.points)
            top.set_stroke(width = 0)
            top.set_fill(disk.get_color())
            top.rotate(np.pi/2, RIGHT)
            top.move_to(disk, UP)
            bottom = top.copy()
            bottom.move_to(disk, DOWN)
            group = VGroup(disk, top, bottom)
            group.truly_original_state = group.copy()
            group.next_to(peg, UP, 0)
            group.rotate_in_place(-np.pi/24, RIGHT)
            group.save_state()
            group.rotate_in_place(-11*np.pi/24, RIGHT)
            disk.set_fill(opacity = 0)
            disk_groups.add(group)
        disk_groups.arrange()
        disk_groups.next_to(self.peg_labels, DOWN)
        
        self.play(FadeIn(
            disk_groups, 
            run_time = 2, 
            lag_ratio = 0.5
        ))
        for group in reversed(list(disk_groups)):
            self.play(group.restore)
            self.play(Transform(group, group.truly_original_state))
        self.remove(disk_groups)
        self.add(self.disks)

    def note_disk_labels(self):
        labels = [disk.label for disk in self.disks]
        last = VGroup().save_state()
        for label in labels:
            label.save_state()
            self.play(
                label.scale_in_place, 2,
                label.set_color, YELLOW,
                last.restore,
                run_time = 0.5
            )
            last = label
        self.play(last.restore)
        self.wait()

    def show_more_disk_possibility(self):
        original_num_disks = self.num_disks
        original_disk_height = self.disk_height
        original_disks = self.disks
        original_disks_copy = original_disks.copy()

        #Hacky
        self.num_disks = 10
        self.disk_height = 0.3
        self.add_disks()
        new_disks = self.disks
        self.disks = original_disks
        self.remove(new_disks)

        self.play(Transform(self.disks, new_disks))
        self.wait()
        self.play(Transform(self.disks, original_disks_copy))

        self.remove(self.disks)
        self.disks = original_disks_copy
        self.add(self.disks)
        self.wait()

        self.num_disks = original_num_disks
        self.disk_height = original_disk_height

    def move_full_tower(self):
        self.move_subtower_to_peg(self.num_disks, 1, run_time = 2)
        self.wait()
        self.reset_disks(run_time = 1, lag_ratio = 0.5)
        self.wait()

    def move_single_disk(self):
        for x in 0, 1, 0:
            self.move_disk(x)
        self.wait()

    def cannot_move_disk_onto_smaller_disk(self):
        also_not_allowed = TextMobject("Not allowed")
        also_not_allowed.to_edge(UP)
        also_not_allowed.set_color(RED)
        cross = TexMobject("\\times")
        cross.set_fill(RED, opacity = 0.5)

        disk = self.disks[2]
        disk.save_state()
        self.move_disks_to_peg([2], 2, added_anims = [
            Transform(self.title, also_not_allowed, run_time = 1)
        ])
        cross.replace(disk)
        self.play(FadeIn(cross))
        self.wait()
        self.play(
            FadeOut(cross),
            FadeOut(self.title),
            disk.restore
        )
        self.wait()

class ExampleFirstMoves(TowersOfHanoiScene):
    def construct(self):
        ruler_sequence = get_ruler_sequence(4)
        cross = TexMobject("\\times")
        cross.set_fill(RED, 0.7)

        self.wait()
        self.play(
            self.disks[0].set_fill, YELLOW,
            self.disks[0].label.set_color, BLACK
        )
        self.wait()
        self.move_disk(0)
        self.wait()
        self.play(
            self.disks[1].set_fill, YELLOW_D,
            self.disks[1].label.set_color, BLACK
        )
        self.move_disk_to_peg(1, 1)
        cross.replace(self.disks[1])
        self.play(FadeIn(cross))
        self.wait()
        self.move_disk_to_peg(1, 2, added_anims = [FadeOut(cross)])
        self.wait()
        for x in ruler_sequence[2:9]:
            self.move_disk(x)
        for x in ruler_sequence[9:]:
            self.move_disk(x, run_time = 0.5, stay_on_peg = False)
        self.wait()

class KeithShowingBinary(Scene):
    def construct(self):
        keith = Keith()
        morty = Mortimer()
        morty.to_corner(DOWN+RIGHT)
        keith.next_to(morty, LEFT, buff = 2*LARGE_BUFF)
        randy = Randolph()
        randy.next_to(keith, LEFT, buff = 2*LARGE_BUFF)
        randy.bubble = randy.get_bubble(SpeechBubble)
        randy.bubble.set_fill(BLACK, opacity = 1)
        randy.bubble.write("Hold on...how does \\\\ binary work again?")

        binary_tex_mobs = get_binary_tex_mobs(list(range(16)))
        binary_tex_mobs.shift(keith.get_corner(UP+LEFT))
        binary_tex_mobs.shift(0.5*(UP+RIGHT))
        bits_list = binary_tex_mobs.split()
        bits = bits_list.pop(0)

        def get_bit_flip():
            return Transform(
                bits, bits_list.pop(0),
                rate_func = squish_rate_func(smooth, 0, 0.7)
            )

        self.play(
            keith.change_mode, "wave_1",
            keith.look_at, bits,
            morty.look_at, bits,
            Write(bits)
        )
        for x in range(2):
            self.play(get_bit_flip())
        self.play(
            morty.change_mode, "pondering",
            morty.look_at, bits,
            get_bit_flip()
        )
        while bits_list:
            added_anims = []
            if random.random() < 0.2:
                if random.random() < 0.5:
                    added_anims.append(Blink(keith))
                else:
                    added_anims.append(Blink(morty))
            self.play(get_bit_flip(), *added_anims)
        self.wait()
        self.play(
            FadeIn(randy),
            morty.change_mode, "plain",
            morty.look_at, randy.eyes,
            keith.change_mode, "plain",
            keith.look_at, randy.eyes,
        )
        self.play(
            randy.change_mode, "confused",
            ShowCreation(randy.bubble),
            Write(randy.bubble.content)
        )
        self.play(Blink(randy))
        self.wait()
        self.play(morty.change_mode, "hooray")
        self.play(Blink(morty))
        self.wait()

class FocusOnRhythm(Scene):
    def construct(self):
        title = TextMobject("Focus on rhythm")
        title.scale(1.5)
        letters = list(reversed(title[-6:]))
        self.play(Write(title, run_time = 1))
        sequence = get_ruler_sequence(5)
        for x in sequence:
            movers = VGroup(*letters[:x+1])
            self.play(
                movers.shift, 0.2*DOWN,
                rate_func = there_and_back,
                run_time = 0.25
            )

class IntroduceBase10(Scene):
    def construct(self):
        self.expand_example_number()
        self.list_digits()

    def expand_example_number(self):
        title = TextMobject("``Base 10''")
        title.to_edge(UP)
        number = TexMobject("137")
        number.next_to(title, DOWN)
        number.shift(2*LEFT)

        colors = [RED, MAROON_B, YELLOW]
        expansion = TexMobject(
            "1(100) + ",
            "3(10) + ",
            "7"
        )
        expansion.next_to(number, DOWN, buff = LARGE_BUFF, aligned_edge = RIGHT)        
        arrows = VGroup()
        number.generate_target()

        for color, digit, term in zip(colors, number.target, expansion):
            digit.set_color(color)
            term.set_color(color)
            arrow = Arrow(digit, term.get_top())
            arrow.set_color(color)
            arrows.add(arrow)
        expansion.save_state()
        for digit, term in zip(number, expansion):
            Transform(term, digit).update(1)

        self.play(
            MoveToTarget(number),
            ShowCreation(arrows),
            ApplyMethod(
                expansion.restore, lag_ratio = 0.5),
            run_time = 2
        )
        self.play(Write(title))
        self.wait()
        self.title = title

    def list_digits(self):
        digits = TextMobject("""
            0, 1, 2, 3, 4,
            5, 6, 7, 8, 9
        """)
        digits.next_to(self.title, DOWN, buff = LARGE_BUFF)
        digits.shift(2*RIGHT)
        self.play(Write(digits))
        self.wait()

class RhythmOfDecimalCounting(CountingScene):
    CONFIG = {
        "ones_configuration_location" : 2*UP+2*RIGHT,
        "num_start_location" : DOWN
    }
    def construct(self):
        for x in range(10):
            self.increment()
        brace = Brace(self.number_mob)
        two_digits = brace.get_text("Two digits")
        one_brace = Brace(self.number_mob[-1])
        tens_place = one_brace.get_text("Ten's place")
        ten_group = self.curr_configurations[1][0]

        self.play(
            GrowFromCenter(brace),
            Write(two_digits, run_time = 1)
        )
        self.wait(2)
        self.play(
            Transform(brace, one_brace),
            Transform(two_digits, tens_place)
        )
        self.wait()
        ten_group.save_state()
        self.play(
            ten_group.scale_in_place, 7,
            ten_group.shift, 2*(DOWN+LEFT),
        )
        self.wait()
        self.play(
            ten_group.restore,
            *list(map(FadeOut, [brace, two_digits]))
        )

        for x in range(89):
            self.increment(run_time_per_anim = 0.25)
        self.increment(run_time_per_anim = 1)
        self.wait()

        hundred_group = self.curr_configurations[2][0]
        hundred_group.save_state()
        self.play(
            hundred_group.scale, 14,
            hundred_group.to_corner, DOWN+LEFT
        )
        self.wait()
        self.play(hundred_group.restore)
        self.wait()
        groups = [
            VGroup(*pair)
            for pair in zip(self.dot_templates, self.curr_configurations)
        ]
        self.play(
            groups[2].to_edge, RIGHT,
            MaintainPositionRelativeTo(groups[1], groups[2]),
            MaintainPositionRelativeTo(groups[0], groups[2]),
            self.number_mob.to_edge, RIGHT, LARGE_BUFF,
            FadeOut(self.arrows)
        )

class DecimalCountingAtHundredsScale(CountingScene):
    CONFIG = {
        "power_colors" : [RED, GREEN, BLUE, PURPLE_D],
        "counting_dot_starting_position" : (FRAME_X_RADIUS+1)*RIGHT + (FRAME_Y_RADIUS-2)*UP,
        "ones_configuration_location" : 2*UP+5.7*RIGHT,
        "num_start_location" : DOWN + 3*RIGHT
    }
    def construct(self):
        added_zeros = TexMobject("00")
        added_zeros.scale(self.num_scale_factor)
        added_zeros.next_to(self.number_mob, RIGHT, SMALL_BUFF, aligned_edge = DOWN)
        added_zeros.set_color_by_gradient(MAROON_B, YELLOW)
        self.add(added_zeros)
        self.increment(run_time_per_anim = 0)

        VGroup(self.number_mob, added_zeros).to_edge(RIGHT, buff = LARGE_BUFF)
        VGroup(self.dot_templates[0], self.curr_configurations[0]).to_edge(RIGHT)
        Transform(
            self.arrows[0], 
            Arrow(self.number_mob, self.dot_templates[0], color = self.power_colors[0])
        ).update(1)

        for x in range(10):
            this_range = list(range(8)) if x == 0 else list(range(9))
            for y in this_range:
                self.increment(run_time_per_anim = 0.25)
            self.increment(run_time_per_anim = 1)

class IntroduceBinaryCounting(BinaryCountingScene):
    CONFIG = {
        "ones_configuration_location" : UP+5*RIGHT,
        "num_start_location" : DOWN+2*RIGHT
    }
    def construct(self):
        self.introduce_name()
        self.initial_counting()
        self.show_self_similarity()

    def introduce_name(self):
        title = TextMobject("Binary (base 2):", "0, 1")
        title.to_edge(UP)
        self.add(title)
        self.number_mob.set_fill(opacity = 0)

        brace = Brace(title[1], buff = SMALL_BUFF)
        bits = TextMobject("bi", "ts", arg_separator = "")
        bits.submobjects.insert(1, VectorizedPoint(bits.get_center()))
        binary_digits = TextMobject("bi", "nary digi", "ts", arg_separator = "")
        for mob in bits, binary_digits:
            mob.next_to(brace, DOWN, buff = SMALL_BUFF)
        VGroup(brace, bits, binary_digits).set_color(BLUE)
        binary_digits[1].set_color(BLUE_E)
        self.play(
            GrowFromCenter(brace),
            Write(bits)
        )
        self.wait()
        bits.save_state()
        self.play(Transform(bits, binary_digits))
        self.wait()
        self.play(bits.restore)
        self.wait()

    def initial_counting(self):
        randy = Randolph().to_corner(DOWN+LEFT)
        bubble = randy.get_bubble(ThoughtBubble, height = 3.4, width = 5)
        bubble.write(
            "Not ten, not ten \\\\",
            "\\quad not ten, not ten..."
        )

        self.play(self.number_mob.set_fill, self.power_colors[0], 1)
        self.increment()
        self.wait()
        self.start_dot = self.curr_configurations[0][0]

        ##Up to 10
        self.increment()
        brace = Brace(self.number_mob[1])
        twos_place = brace.get_text("Two's place")
        self.play(
            GrowFromCenter(brace),
            Write(twos_place)
        )
        self.play(
            FadeIn(randy),
            ShowCreation(bubble)
        )
        self.play(
            randy.change_mode, "hesitant",
            randy.look_at, self.number_mob,
            Write(bubble.content)
        )
        self.wait()
        curr_content = bubble.content
        bubble.write("$1 \\! \\cdot \\! 2+$", "$0$")
        bubble.content[0][0].set_color(self.power_colors[1])
        self.play(
            Transform(curr_content, bubble.content),
            randy.change_mode, "pondering",
            randy.look_at, self.number_mob
        )
        self.remove(curr_content)
        self.add(bubble.content)

        #Up to 11
        zero = bubble.content[-1]
        zero.set_color(self.power_colors[0])
        one = TexMobject("1").replace(zero, dim_to_match = 1)
        one.set_color(zero.get_color())
        self.play(Blink(randy))
        self.increment(added_anims = [Transform(zero, one)])
        self.wait()

        #Up to 100
        curr_content = bubble.content
        bubble.write(
            "$1 \\!\\cdot\\! 4 + $",
            "$0 \\!\\cdot\\! 2 + $",
            "$0$",
        )
        colors = reversed(self.power_colors[:3])
        for piece, color in zip(bubble.content.submobjects, colors):
            piece[0].set_color(color)
        self.increment(added_anims = [Transform(curr_content, bubble.content)])
        four_brace = Brace(self.number_mob[-1])
        fours_place = four_brace.get_text("Four's place")
        self.play(
            Transform(brace, four_brace),
            Transform(twos_place, fours_place),
        )
        self.play(Blink(randy))
        self.play(*list(map(FadeOut, [bubble, curr_content])))

        #Up to 1000
        for x in range(4):
            self.increment()
        brace.target = Brace(self.number_mob[-1])
        twos_place.target = brace.get_text("Eight's place")
        self.play(
            randy.change_mode, "happy",
            randy.look_at, self.number_mob,
            *list(map(MoveToTarget, [brace, twos_place]))
        )
        for x in range(8):
            self.increment(total_run_time = 1)
        self.wait()
        for x in range(8):
            self.increment(total_run_time = 1.5)

    def show_self_similarity(self):
        cover_rect = Rectangle()
        cover_rect.set_width(FRAME_WIDTH)
        cover_rect.set_height(FRAME_HEIGHT)
        cover_rect.set_stroke(width = 0)
        cover_rect.set_fill(BLACK, opacity = 0.85)
        big_dot = self.curr_configurations[-1][0].copy()
        self.play(
            FadeIn(cover_rect),
            Animation(big_dot)
        )
        self.play(
            big_dot.center,
            big_dot.set_height, FRAME_HEIGHT-2,
            big_dot.to_edge, LEFT,
            run_time = 5
        )

class BinaryCountingAtEveryScale(Scene):
    CONFIG = {
        "num_bits" : 4,
        "show_title" : False,
    }
    def construct(self):
        title = TextMobject("Count to %d (which is %s in binary)"%(
            2**self.num_bits-1, bin(2**self.num_bits-1)[2:]
        ))
        title.to_edge(UP)
        if self.show_title:
            self.add(title)

        bit_mobs = [
            get_binary_tex_mob(n, self.num_bits)
            for n in range(2**self.num_bits)
        ]
        curr_bits = bit_mobs[0]

        lower_brace = Brace(VGroup(*curr_bits[1:]))
        do_a_thing = lower_brace.get_text("Do a thing")
        VGroup(lower_brace, do_a_thing).set_color(YELLOW)
        upper_brace = Brace(curr_bits, UP)
        roll_over = upper_brace.get_text("Roll over")
        VGroup(upper_brace, roll_over).set_color(MAROON_B)
        again = TextMobject("again")
        again.next_to(do_a_thing, RIGHT, 2*SMALL_BUFF)
        again.set_color(YELLOW)

        self.add(curr_bits, lower_brace, do_a_thing)

        def get_run_through(mobs):
            return Succession(*[
                Transform(
                    curr_bits, mob, 
                    rate_func = squish_rate_func(smooth, 0, 0.5)
                )
                for mob in mobs
            ], run_time = 1)

        for bit_mob in bit_mobs:
            curr_bits.align_data(bit_mob)
            bit_mob.set_color(YELLOW)
            bit_mob[0].set_color(MAROON_B)
        self.play(get_run_through(bit_mobs[1:2**(self.num_bits-1)]))
        self.play(*list(map(FadeIn, [upper_brace, roll_over])))
        self.play(Transform(
            VGroup(*reversed(list(curr_bits))),
            VGroup(*reversed(list(bit_mobs[2**(self.num_bits-1)]))),
            lag_ratio = 0.5,
        ))
        self.wait()
        self.play(
            get_run_through(bit_mobs[2**(self.num_bits-1)+1:]),
            Write(again)
        )
        self.wait()

class BinaryCountingAtSmallestScale(BinaryCountingAtEveryScale):
    CONFIG = {
        "num_bits" : 2,
        "show_title" : True,
    }

class BinaryCountingAtMediumScale(BinaryCountingAtEveryScale):
    CONFIG = {
        "num_bits" : 4,
        "show_title" : True,
    }

class BinaryCountingAtLargeScale(BinaryCountingAtEveryScale):
    CONFIG = {
        "num_bits" : 8,
        "show_title" : True,
    }

class IntroduceSolveByCounting(TowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 4
    }
    def construct(self):
        self.initialize_bit_mobs()
        for disk in self.disks:
            disk.original_fill_color = disk.get_color()
        braces = [
            Brace(VGroup(*self.curr_bit_mob[:n]))
            for n in range(1, self.num_disks+1)
        ]
        word_list = [
            brace.get_text(text)
            for brace, text in zip(braces, [
                "Only flip last bit",
                "Roll over once",
                "Roll over twice",
                "Roll over three times",
            ])
        ]
        brace = braces[0].copy()
        words = word_list[0].copy()

        ##First increment
        self.play(self.get_increment_animation())
        self.play(
            GrowFromCenter(brace),
            Write(words, run_time = 1)
        )
        disk = self.disks[0]
        last_bit = self.curr_bit_mob[0]
        last_bit.save_state()
        self.play(
            disk.set_fill, YELLOW,
            disk[1].set_fill, BLACK,
            last_bit.set_fill, YELLOW,
        )
        self.wait()
        self.move_disk(0, run_time = 2)
        self.play(
            last_bit.restore,
            disk.set_fill, disk.original_fill_color,
            self.disks[0][1].set_fill, BLACK
        )

        ##Second increment
        self.play(
            self.get_increment_animation(),
            Transform(words, word_list[1]),
            Transform(brace, braces[1]),
        )
        disk = self.disks[1]
        twos_bit = self.curr_bit_mob[1]
        twos_bit.save_state()
        self.play(
            disk.set_fill, MAROON_B,
            disk[1].set_fill, BLACK,
            twos_bit.set_fill, MAROON_B,
        )
        self.move_disk(1, run_time = 2)
        self.wait()
        self.move_disk_to_peg(1, 1, stay_on_peg = False)
        cross = TexMobject("\\times")
        cross.replace(disk)
        cross.set_fill(RED, opacity = 0.5)
        self.play(FadeIn(cross))
        self.wait()
        self.move_disk_to_peg(
            1, 2, stay_on_peg = False, 
            added_anims = [FadeOut(cross)]
        )
        self.play(
            disk.set_fill, disk.original_fill_color,
            disk[1].set_fill, BLACK,
            twos_bit.restore,
            Transform(brace, braces[0]),
            Transform(words, word_list[0]),
        )
        self.move_disk(
            0, 
            added_anims = [self.get_increment_animation()],
            run_time = 2
        )
        self.wait()

        ##Fourth increment
        self.play(
            Transform(brace, braces[2]),
            Transform(words, word_list[2]),
        )
        self.play(self.get_increment_animation())
        disk = self.disks[2]
        fours_bit = self.curr_bit_mob[2]
        fours_bit.save_state()
        self.play(
            disk.set_fill, RED,
            disk[1].set_fill, BLACK,
            fours_bit.set_fill, RED
        )
        self.move_disk(2, run_time = 2)
        self.play(
            disk.set_fill, disk.original_fill_color,
            disk[1].set_fill, BLACK,
            fours_bit.restore,
            FadeOut(brace),
            FadeOut(words)
        )
        self.wait()
        for disk_index in 0, 1, 0:
            self.play(self.get_increment_animation())
            self.move_disk(disk_index)
        self.wait()

        ##Eighth incremement
        brace = braces[3]
        words = word_list[3]
        self.play(
            self.get_increment_animation(),
            GrowFromCenter(brace),
            Write(words, run_time = 1)
        )
        disk = self.disks[3]
        eights_bit = self.curr_bit_mob[3]
        eights_bit.save_state()
        self.play(
            disk.set_fill, GREEN,
            disk[1].set_fill, BLACK,
            eights_bit.set_fill, GREEN
        )
        self.move_disk(3, run_time = 2)
        self.play(
            disk.set_fill, disk.original_fill_color,
            disk[1].set_fill, BLACK,
            eights_bit.restore,
        )
        self.play(*list(map(FadeOut, [brace, words])))
        for disk_index in get_ruler_sequence(2):
            self.play(self.get_increment_animation())
            self.move_disk(disk_index, stay_on_peg = False)
        self.wait()

    def initialize_bit_mobs(self):
        bit_mobs = VGroup(*[
            get_binary_tex_mob(n, self.num_disks)
            for n in range(2**(self.num_disks))
        ])
        bit_mobs.scale(2)
        self.bit_mobs_iter = it.cycle(bit_mobs)
        self.curr_bit_mob = next(self.bit_mobs_iter)

        for bit_mob in bit_mobs:
            bit_mob.align_data(self.curr_bit_mob)
            for bit, disk in zip(bit_mob, reversed(list(self.disks))):
                bit.set_color(disk.get_color())
        bit_mobs.next_to(self.peg_labels, DOWN)

        self.add(self.curr_bit_mob)

    def get_increment_animation(self):
        return Succession(
            Transform(
                self.curr_bit_mob, next(self.bit_mobs_iter),
                lag_ratio = 0.5,
                path_arc = -np.pi/3
            ),
            Animation(self.curr_bit_mob)
        )

class SolveSixDisksByCounting(IntroduceSolveByCounting):
    CONFIG = {
        "num_disks" : 6,
        "stay_on_peg" : False,
        "run_time_per_move" : 0.5,
    }
    def construct(self):
        self.initialize_bit_mobs()
        for disk_index in get_ruler_sequence(self.num_disks-1):
            self.play(
                self.get_increment_animation(),
                run_time = self.run_time_per_move,
            )
            self.move_disk(
                disk_index,
                stay_on_peg = self.stay_on_peg,
                run_time = self.run_time_per_move,
            )
        self.wait()

class RecursionTime(Scene):
    def construct(self):
        keith = Keith().shift(2*DOWN+3*LEFT)
        morty = Mortimer().shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)

        keith_kick = keith.copy().change_mode("dance_kick")
        keith_kick.scale_in_place(1.3)
        keith_kick.shift(0.5*LEFT)
        keith_kick.look_at(morty.eyes)
        keith_hooray = keith.copy().change_mode("hooray")

        self.add(keith, morty)

        bubble = keith.get_bubble(SpeechBubble, height = 2)
        bubble.write("Recursion time!!!")
        VGroup(bubble, bubble.content).shift(UP)

        self.play(
            Transform(keith, keith_kick),
            morty.change_mode, "happy",
            ShowCreation(bubble),
            Write(bubble.content, run_time = 1)
        )
        self.play(
            morty.change_mode, "hooray",
            Transform(keith, keith_hooray),
            bubble.content.set_color_by_gradient, BLUE_A, BLUE_E
        )
        self.play(Blink(morty))
        self.wait()

class RecursiveSolution(TowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 4,
        "middle_peg_bottom" : 2*DOWN,
    }
    def construct(self):
        # VGroup(*self.get_mobjects()).shift(1.5*DOWN)
        big_disk = self.disks[-1]
        self.eyes = Eyes(big_disk)
        title = TextMobject("Move 4-tower")
        sub_steps = TextMobject(
            "Move 3-tower,",
            "Move disk 3,",
            "Move 3-tower",
        )
        sub_steps[1].set_color(GREEN)
        sub_step_brace = Brace(sub_steps, UP)
        sub_sub_steps = TextMobject(
            "Move 2-tower,",
            "Move disk 2,",
            "Move 2-tower",
        )
        sub_sub_steps[1].set_color(RED)
        sub_sub_steps_brace = Brace(sub_sub_steps, UP)
        steps = VGroup(
            title, sub_step_brace, sub_steps, 
            sub_sub_steps_brace, sub_sub_steps
        )
        steps.arrange(DOWN)
        steps.scale(0.7)
        steps.to_edge(UP)
        VGroup(sub_sub_steps_brace, sub_sub_steps).next_to(sub_steps[-1], DOWN)

        self.add(title)

        ##Big disk is frustrated
        self.play(
            FadeIn(self.eyes),
            big_disk.set_fill, GREEN,
            big_disk.label.set_fill, BLACK,
        )
        big_disk.add(self.eyes)        
        self.blink()
        self.wait()
        self.change_mode("angry")
        for x in range(2):
            self.wait()
            self.shake(big_disk)
            self.blink()
            self.wait()
        self.change_mode("plain")
        self.look_at(self.peg_labels[2])
        self.look_at(self.disks[0])
        self.blink()

        #Subtower move
        self.move_subtower_to_peg(3, 1, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[1]),
            FadeIn(sub_step_brace),
            Write(sub_steps[0], run_time = 1)
        ])
        self.wait()
        self.move_disk_to_peg(0, 0, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[0].get_top())
        ])
        self.shake(big_disk)
        self.move_disk_to_peg(0, 2, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2].get_bottom())
        ])
        self.change_mode("angry")
        self.move_disk_to_peg(0, 1, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.disks[1].get_top())
        ])
        self.blink()

        #Final moves for big case
        self.move_disk(3, run_time = 2, added_anims = [
            Write(sub_steps[1])
        ])
        self.look_at(self.disks[1])
        self.blink()
        bubble = SpeechBubble()
        bubble.write("I'm set!")
        bubble.resize_to_content()
        bubble.pin_to(big_disk)
        bubble.add_content(bubble.content)
        bubble.set_fill(BLACK, opacity = 0.7)
        self.play(
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.wait()
        self.blink()
        self.play(*list(map(FadeOut, [bubble, bubble.content])))
        big_disk.remove(self.eyes)
        self.move_subtower_to_peg(3, 2, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2].get_top()),
            Write(sub_steps[2])
        ])
        self.play(FadeOut(self.eyes))
        self.wait()

        #Highlight subproblem
        self.play(
            VGroup(*self.disks[:3]).move_to, self.pegs[1], DOWN
        )
        self.disk_tracker = [set([]), set([0, 1, 2]), set([3])]
        arc = Arc(-5*np.pi/6, start_angle = 5*np.pi/6)
        arc.add_tip()
        arc.set_color(YELLOW)
        arc.set_width(
            VGroup(*self.pegs[1:]).get_width()*0.8
        )
        arc.next_to(self.disks[0], UP+RIGHT, buff = SMALL_BUFF)
        q_mark = TextMobject("?")
        q_mark.next_to(arc, UP)
        self.play(
            ShowCreation(arc),
            Write(q_mark),
            sub_steps[-1].set_color, YELLOW
        )
        self.wait()
        self.play(
            GrowFromCenter(sub_sub_steps_brace),
            *list(map(FadeOut, [arc, q_mark]))
        )

        #Disk 2 frustration
        big_disk = self.disks[2]
        self.eyes.move_to(big_disk.get_top(), DOWN)
        self.play(
            FadeIn(self.eyes),
            big_disk.set_fill, RED,
            big_disk.label.set_fill, BLACK
        )
        big_disk.add(self.eyes)
        self.change_mode("sad")
        self.look_at(self.pegs[1].get_top())
        self.shake(big_disk)
        self.blink()

        #Move sub-sub-tower
        self.move_subtower_to_peg(2, 0, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[0].get_bottom()),
            Write(sub_sub_steps[0])
        ])
        self.blink()
        self.move_disk_to_peg(2, 2, run_time = 2, added_anims = [
            Write(sub_sub_steps[1])
        ])
        self.look_at(self.disks[0])
        big_disk.remove(self.eyes)
        self.move_subtower_to_peg(2, 2, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2].get_top()),
            Write(sub_sub_steps[2])
        ])
        self.blink()
        self.look_at(self.disks[-1])

        #Move eyes
        self.play(FadeOut(self.eyes))
        self.eyes.move_to(self.disks[1].get_top(), DOWN)
        self.play(FadeIn(self.eyes))
        self.blink()
        self.play(FadeOut(self.eyes))
        self.eyes.move_to(self.disks[3].get_top(), DOWN)
        self.play(FadeIn(self.eyes))

        #Show process one last time
        big_disk = self.disks[3]
        big_disk.add(self.eyes)
        self.move_subtower_to_peg(3, 1, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[0])
        ])
        self.move_disk_to_peg(3, 0, run_time = 2)
        big_disk.remove(self.eyes)
        self.move_subtower_to_peg(3, 0, run_time = 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[0].get_top())
        ])
        self.blink()

    def shake(self, mobject, direction = UP, added_anims = []):
        self.play(
            mobject.shift, 0.2*direction, rate_func = wiggle,
            *added_anims
        )

    def blink(self):
        self.play(self.eyes.blink_anim())

    def look_at(self, point_or_mobject):
        self.play(self.eyes.look_at_anim(point_or_mobject))

    def change_mode(self, mode):
        self.play(self.eyes.change_mode_anim(mode))

class KeithSaysBigToSmall(Scene):
    def construct(self):
        keith = Keith()
        keith.shift(2.5*DOWN + 3*LEFT)
        bubble = keith.get_bubble(SpeechBubble, height = 4.5)
        bubble.write("""
            Big problem
            $\\Downarrow$
            Smaller problem
        """)

        self.add(keith)
        self.play(Blink(keith))
        self.play(
            keith.change_mode, "speaking",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.wait()
        self.play(Blink(keith))
        self.wait()

class CodeThisUp(Scene):
    def construct(self):
        keith = Keith()
        keith.shift(2*DOWN+3*LEFT)
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)
        point = 2*UP+3*RIGHT
        bubble = keith.get_bubble(SpeechBubble, width = 4.5, height = 3)
        bubble.write("This is the \\\\ most efficient")
        self.add(morty, keith)

        self.play(
            keith.change_mode, "speaking",
            keith.look_at, point
        )
        self.play(
            morty.change_mode, "pondering",
            morty.look_at, point
        )
        self.play(Blink(keith))
        self.wait(2)
        self.play(Blink(morty))
        self.wait()
        self.play(
            keith.change_mode, "hooray",
            keith.look_at, morty.eyes
        )
        self.play(Blink(keith))
        self.wait()
        self.play(
            keith.change_mode, "speaking",
            keith.look_at, morty.eyes,
            ShowCreation(bubble),
            Write(bubble.content),
            morty.change_mode, "happy",
            morty.look_at, keith.eyes,
        )
        self.wait()
        self.play(Blink(morty))
        self.wait()

class HanoiSolutionCode(Scene):
    def construct(self):
        pass

class NoRoomForInefficiency(Scene):
    def construct(self):
        morty = Mortimer().flip()
        morty.shift(2.5*DOWN+3*LEFT)
        bubble = morty.get_bubble(SpeechBubble, width = 4)
        bubble.write("No room for \\\\ inefficiency")
        VGroup(morty, bubble, bubble.content).to_corner(DOWN+RIGHT)

        self.add(morty)
        self.play(
            morty.change_mode, "speaking",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait()

class WhyDoesBinaryAchieveThis(Scene):
    def construct(self):
        keith = Keith()
        keith.shift(2*DOWN+3*LEFT)
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)
        bubble = morty.get_bubble(SpeechBubble, width = 5, height = 3)
        bubble.write("""
            Why does counting
            in binary work?
        """)
        self.add(morty, keith)

        self.play(
            morty.change_mode, "confused",
            morty.look_at, keith.eyes,
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(keith.change_mode, "happy")
        self.wait()
        self.play(Blink(morty))
        self.wait()

class BothAreSelfSimilar(Scene):
    def construct(self):
        morty = Mortimer().flip()
        morty.shift(2.5*DOWN+3*LEFT)
        bubble = morty.get_bubble(SpeechBubble)
        bubble.write("Both are self-similar")

        self.add(morty)
        self.play(
            morty.change_mode, "hooray",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait()

class LargeScaleHanoiDecomposition(TowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 8,
        "peg_height" : 3.5,
        "middle_peg_bottom" : 2*DOWN,
        "disk_max_width" : 4,
    }
    def construct(self):
        self.move_subtower_to_peg(7, 1, stay_on_peg = False)
        self.wait()
        self.move_disk(7, stay_on_peg = False)
        self.wait()
        self.move_subtower_to_peg(7, 2, stay_on_peg = False)
        self.wait()

class SolveTwoDisksByCounting(SolveSixDisksByCounting):
    CONFIG = {
        "num_disks" : 2,
        "stay_on_peg" : False,
        "run_time_per_move" : 1,
        "disk_max_width" : 1.5,
    }
    def construct(self):
        self.initialize_bit_mobs()
        for disk_index in 0, 1, 0:
            self.play(self.get_increment_animation())
            self.move_disk(
                disk_index, 
                stay_on_peg = False,
            )
            self.wait()

class ShowFourDiskFourBitsParallel(IntroduceSolveByCounting):
    CONFIG = {
        "num_disks" : 4,
        "subtask_run_time" : 1,
    }
    def construct(self):
        self.initialize_bit_mobs()
        self.counting_subtask()
        self.wait()
        self.disk_subtask()
        self.wait()
        self.play(self.get_increment_animation())
        self.move_disk(
            self.num_disks-1, 
            stay_on_peg = False,
        )
        self.wait()
        self.counting_subtask()
        self.wait()
        self.disk_subtask()
        self.wait()

    def disk_subtask(self):
        sequence = get_ruler_sequence(self.num_disks-2)
        run_time = float(self.subtask_run_time)/len(sequence)
        for disk_index in get_ruler_sequence(self.num_disks-2):
            self.move_disk(
                disk_index, 
                run_time = run_time,
                stay_on_peg = False,
            )
        # curr_peg = self.disk_index_to_peg_index(0)
        # self.move_subtower_to_peg(self.num_disks-1, curr_peg+1)

    def counting_subtask(self):
        num_tasks = 2**(self.num_disks-1)-1
        run_time = float(self.subtask_run_time)/num_tasks
        # for x in range(num_tasks):
        #     self.play(
        #         self.get_increment_animation(),
        #         run_time = run_time
        #     )
        self.play(
            Succession(*[
                self.get_increment_animation()
                for x in range(num_tasks)
            ]),
            rate_func=linear,
            run_time = self.subtask_run_time
        )

    def get_increment_animation(self):
        return Transform(
            self.curr_bit_mob, next(self.bit_mobs_iter),
            path_arc = -np.pi/3,
        )
 
class ShowThreeDiskThreeBitsParallel(ShowFourDiskFourBitsParallel):
    CONFIG = {
        "num_disks" : 3,
        "subtask_run_time" : 1
    }

class ShowFiveDiskFiveBitsParallel(ShowFourDiskFourBitsParallel):
    CONFIG = {
        "num_disks" : 5,
        "subtask_run_time" : 2
    }

class ShowSixDiskSixBitsParallel(ShowFourDiskFourBitsParallel):
    CONFIG = {
        "num_disks" : 6,
        "subtask_run_time" : 2
    }

class CoolRight(Scene):
    def construct(self):
        morty = Mortimer()
        morty.shift(2*DOWN)
        bubble = SpeechBubble()
        bubble.write("Cool! right?")
        bubble.resize_to_content()
        bubble.pin_to(morty)

        self.play(
            morty.change_mode, "surprised",
            morty.look, OUT,
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait()
        curr_content = bubble.content
        bubble.write("It gets \\\\ better...")
        self.play(
            Transform(curr_content, bubble.content),
            morty.change_mode, "hooray",
            morty.look, OUT
        )
        self.wait()
        self.play(Blink(morty))
        self.wait()

############ Part 2 ############

class MentionLastVideo(Scene):
    def construct(self):
        keith = Keith()
        keith.shift(2*DOWN+3*LEFT)
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)
        point = 2*UP

        name = TextMobject("""
            Keith Schwarz
            (Computer Scientist)
        """)
        name.to_corner(UP+LEFT)
        arrow = Arrow(name.get_bottom(), keith.get_top())

        self.add(morty, keith)
        self.play(
            keith.change_mode, "raise_right_hand",
            keith.look_at, point,
            morty.change_mode, "pondering",
            morty.look_at, point
        )
        self.play(Blink(keith))
        self.play(Write(name))
        self.play(ShowCreation(arrow))
        self.play(Blink(morty))
        self.wait(2)
        self.play(
            morty.change_mode, "confused",
            morty.look_at, point
        )
        self.play(Blink(keith))
        self.wait(2)
        self.play(
            morty.change_mode, "surprised"
        )
        self.wait()

class IntroduceConstrainedTowersOfHanoi(ConstrainedTowersOfHanoiScene):
    CONFIG = {
        "middle_peg_bottom" : 2*DOWN,
    }
    def construct(self):
        title = TextMobject("Constrained", "Towers of Hanoi")
        title.set_color_by_tex("Constrained", YELLOW)
        title.to_edge(UP)

        self.play(Write(title))
        self.add_arcs()
        self.disks.save_state()
        for index in 0, 0, 1, 0:
            self.move_disk(index)
            self.wait()
        self.wait()

        self.play(self.disks.restore)
        self.disk_tracker = [set(range(self.num_disks)), set([]), set([])]
        self.wait()
        self.move_disk_to_peg(0, 1)
        self.move_disk_to_peg(1, 2)
        self.play(ShowCreation(self.big_curved_arrow))
        cross = TexMobject("\\times")
        cross.scale(2)
        cross.set_fill(RED)
        cross.move_to(self.big_curved_arrow.get_top())
        big_cross = cross.copy()
        big_cross.replace(self.disks[1])
        big_cross.set_fill(opacity = 0.5)
        self.play(FadeIn(cross))
        self.play(FadeIn(big_cross))
        self.wait()


    def add_arcs(self):
        arc = Arc(start_angle = np.pi/6, angle = 2*np.pi/3)
        curved_arrow1 = VGroup(arc, arc.copy().flip())
        curved_arrow2 = curved_arrow1.copy()
        curved_arrows = [curved_arrow1, curved_arrow2]
        for curved_arrow in curved_arrows:
            for arc in curved_arrow:
                arc.add_tip(tip_length = 0.15)
                arc.set_color(YELLOW)
        peg_sets = (self.pegs[:2], self.pegs[1:])
        for curved_arrow, pegs in zip(curved_arrows, peg_sets):
            peg_group = VGroup(*pegs)
            curved_arrow.set_width(0.7*peg_group.get_width())
            curved_arrow.next_to(peg_group, UP)

        self.play(ShowCreation(curved_arrow1))
        self.play(ShowCreation(curved_arrow2))
        self.wait()

        big_curved_arrow = Arc(start_angle = 5*np.pi/6, angle = -2*np.pi/3)
        big_curved_arrow.set_width(0.9*self.pegs.get_width())
        big_curved_arrow.next_to(self.pegs, UP)
        big_curved_arrow.add_tip(tip_length = 0.4)
        big_curved_arrow.set_color(WHITE)
        self.big_curved_arrow = big_curved_arrow

class StillRecruse(Scene):
    def construct(self):
        keith = Keith()
        keith.shift(2*DOWN+3*LEFT)
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)
        point = 2*UP+3*RIGHT
        bubble = keith.get_bubble(SpeechBubble, width = 4.5, height = 3)
        bubble.write("You can still\\\\ use recursion")
        self.add(morty, keith)

        self.play(
            keith.change_mode, "speaking",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(morty.change_mode, "hooray")
        self.play(Blink(keith))
        self.wait()
        self.play(Blink(morty))
        self.wait()

class RecursiveSolutionToConstrained(RecursiveSolution):
    CONFIG = {
        "middle_peg_bottom" : 2*DOWN,
        "num_disks" : 4,
    }
    def construct(self):
        big_disk = self.disks[-1]
        self.eyes = Eyes(big_disk)

        #Define steps breakdown text
        title = TextMobject("Move 4-tower")
        subdivisions = [
            TextMobject(
                "\\tiny Move %d-tower,"%d,
                "Move disk %d,"%d,
                "\\, Move %d-tower, \\,"%d,
                "Move disk %d,"%d,
                "Move %d-tower"%d,
            ).set_color_by_tex("Move disk %d,"%d, color)
            for d, color in [(3, GREEN), (2, RED), (1, BLUE_C)]
        ]
        sub_steps, sub_sub_steps = subdivisions[:2]
        for steps in subdivisions:
            steps.set_width(FRAME_WIDTH-1)
        subdivisions.append(
            TextMobject("\\tiny Move disk 0, Move disk 0").set_color(BLUE)
        )
        braces = [
            Brace(steps, UP)
            for steps in subdivisions
        ]
        sub_steps_brace, sub_sub_steps_brace = braces[:2]
        steps = VGroup(title, *it.chain(*list(zip(
            braces, subdivisions
        ))))
        steps.arrange(DOWN)
        steps.to_edge(UP)

        steps_to_fade = VGroup(
            title, sub_steps_brace,
            *list(sub_steps[:2]) + list(sub_steps[3:])
        )
        self.add(title)

        #Initially move big disk
        self.play(
            FadeIn(self.eyes),
            big_disk.set_fill, GREEN,
            big_disk.label.set_fill, BLACK
        )
        big_disk.add(self.eyes)
        big_disk.save_state()
        self.blink()
        self.look_at(self.pegs[2])
        self.move_disk_to_peg(self.num_disks-1, 2, stay_on_peg = False)
        self.look_at(self.pegs[0])
        self.blink()
        self.play(big_disk.restore, path_arc = np.pi/3)
        self.disk_tracker = [set(range(self.num_disks)), set([]), set([])]
        self.look_at(self.pegs[0].get_top())
        self.change_mode("angry")
        self.shake(big_disk)
        self.wait()

        #Talk about tower blocking
        tower = VGroup(*self.disks[:self.num_disks-1])
        blocking = TextMobject("Still\\\\", "Blocking")
        blocking.set_color(RED)
        blocking.to_edge(LEFT)
        blocking.shift(2*UP)
        arrow = Arrow(blocking.get_bottom(), tower.get_top(), buff = SMALL_BUFF)
        new_arrow = Arrow(blocking.get_bottom(), self.pegs[1], buff = SMALL_BUFF)
        VGroup(arrow, new_arrow).set_color(RED)

        self.play(
            Write(blocking[1]),
            ShowCreation(arrow)
        )
        self.shake(tower, RIGHT, added_anims = [Animation(big_disk)])
        self.blink()
        self.shake(big_disk)
        self.wait()
        self.move_subtower_to_peg(self.num_disks-1, 1, added_anims = [
            Transform(arrow, new_arrow),
            self.eyes.look_at_anim(self.pegs[1])
        ])
        self.play(Write(blocking[0]))
        self.shake(big_disk, RIGHT)
        self.wait()
        self.blink()
        self.wait()
        self.play(FadeIn(sub_steps_brace))
        self.move_subtower_to_peg(self.num_disks-1, 2, added_anims = [
            FadeOut(blocking),
            FadeOut(arrow),
            self.eyes.change_mode_anim("plain", thing_to_look_at = self.pegs[2]),
            Write(sub_steps[0], run_time = 1),
        ])
        self.blink()

        #Proceed through actual process
        self.move_disk_to_peg(self.num_disks-1, 1, added_anims = [
            Write(sub_steps[1], run_time = 1),
        ])
        self.wait()
        self.move_subtower_to_peg(self.num_disks-1, 0, added_anims = [
            self.eyes.look_at_anim(self.pegs[0]),
            Write(sub_steps[2], run_time = 1),
        ])
        self.blink()
        self.move_disk_to_peg(self.num_disks-1, 2, added_anims = [
            Write(sub_steps[3], run_time = 1),
        ])
        self.wait()
        big_disk.remove(self.eyes)
        self.move_subtower_to_peg(self.num_disks-1, 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2].get_top()),
            Write(sub_steps[4], run_time = 1),
        ])
        self.blink()
        self.play(FadeOut(self.eyes))

        #Ask about subproblem
        sub_sub_steps_brace.set_color(WHITE)
        self.move_subtower_to_peg(self.num_disks-1, 0, added_anims = [
            steps_to_fade.fade, 0.7,
            sub_steps[2].set_color, WHITE,
            sub_steps[2].scale_in_place, 1.2,
            FadeIn(sub_sub_steps_brace)
        ])
        num_disks = self.num_disks-1
        big_disk = self.disks[num_disks-1]
        self.eyes.move_to(big_disk.get_top(), DOWN)
        self.play(
            FadeIn(self.eyes),
            big_disk.set_fill, RED,
            big_disk.label.set_fill, BLACK,
        )
        big_disk.add(self.eyes)        
        self.blink()

        #Solve subproblem
        self.move_subtower_to_peg(num_disks-1, 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2]),
            Write(sub_sub_steps[0], run_time = 1)
        ])
        self.blink()
        self.move_disk_to_peg(num_disks-1, 1, added_anims = [
            Write(sub_sub_steps[1], run_time = 1)
        ])
        self.wait()
        self.move_subtower_to_peg(num_disks-1, 0, added_anims = [
            self.eyes.look_at_anim(self.pegs[0]),
            Write(sub_sub_steps[2], run_time = 1)
        ])
        self.blink()
        self.move_disk_to_peg(num_disks-1, 2, added_anims = [
            Write(sub_sub_steps[3], run_time = 1)
        ])
        self.wait()
        big_disk.remove(self.eyes)
        self.move_subtower_to_peg(num_disks-1, 2, added_anims = [
            self.eyes.look_at_anim(self.pegs[2].get_top()),
            Write(sub_sub_steps[4], run_time = 1)
        ])
        self.wait()

        #Show smallest subdivisions
        smaller_subdivision = VGroup(
            *list(subdivisions[2:]) + \
            list(braces[2:])
        )
        last_subdivisions = [VGroup(braces[-1], subdivisions[-1])]
        for vect in LEFT, RIGHT:
            group = last_subdivisions[0].copy()
            group.to_edge(vect)
            steps.add(group)
            smaller_subdivision.add(group)
            last_subdivisions.append(group)
        smaller_subdivision.set_fill(opacity = 0)
        self.play(
            steps.shift, 
            (FRAME_Y_RADIUS-sub_sub_steps.get_top()[1]-MED_SMALL_BUFF)*UP,
            self.eyes.look_at_anim(steps)
        )
        self.play(ApplyMethod(
            VGroup(VGroup(braces[-2], subdivisions[-2])).set_fill, None, 1,
            run_time = 3,
            lag_ratio = 0.5,
        ))
        self.blink()
        for mob in last_subdivisions:
            self.play(
                ApplyMethod(mob.set_fill, None, 1),
                self.eyes.look_at_anim(mob)
            )
        self.blink()
        self.play(FadeOut(self.eyes))
        self.wait()

        #final movements
        movements = [
            (0, 1),
            (0, 0),
            (1, 1),
            (0, 1),
            (0, 2),
            (1, 0),
            (0, 1),
            (0, 0),
        ]
        for disk_index, peg_index in movements:
            self.move_disk_to_peg(
                disk_index, peg_index, 
                stay_on_peg = False
            )
        self.wait()

class SimpleConstrainedBreakdown(TowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 4
    }
    def construct(self):
        self.move_subtower_to_peg(self.num_disks-1, 2)
        self.wait()
        self.move_disk(self.num_disks-1)
        self.wait()
        self.move_subtower_to_peg(self.num_disks-1, 0)
        self.wait()
        self.move_disk(self.num_disks-1)
        self.wait()
        self.move_subtower_to_peg(self.num_disks-1, 2)
        self.wait()

class SolveConstrainedByCounting(ConstrainedTowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 5,
        "ternary_mob_scale_factor" : 2,
    }
    def construct(self):
        ternary_mobs = VGroup()
        for num in range(3**self.num_disks):
            ternary_mob = get_ternary_tex_mob(num, self.num_disks)
            ternary_mob.scale(self.ternary_mob_scale_factor)
            ternary_mob.set_color_by_gradient(*self.disk_start_and_end_colors)
            ternary_mobs.add(ternary_mob)
        ternary_mobs.next_to(self.peg_labels, DOWN)
        self.ternary_mob_iter = it.cycle(ternary_mobs)
        self.curr_ternary_mob = next(self.ternary_mob_iter)
        self.add(self.curr_ternary_mob)

        for index in get_ternary_ruler_sequence(self.num_disks-1):
            self.move_disk(index, stay_on_peg = False, added_anims = [
                self.increment_animation()
            ])

    def increment_animation(self):
        return Succession(
            Transform(
                self.curr_ternary_mob, next(self.ternary_mob_iter),
                lag_ratio = 0.5,
                path_arc = np.pi/6,
            ),
            Animation(self.curr_ternary_mob),
        )

class CompareNumberSystems(Scene):
    def construct(self):
        base_ten = TextMobject("Base ten")
        base_ten.to_corner(UP+LEFT).shift(RIGHT)
        binary = TextMobject("Binary")
        binary.to_corner(UP+RIGHT).shift(LEFT)
        ternary = TextMobject("Ternary")
        ternary.to_edge(UP)
        ternary.set_color(YELLOW)
        titles = [base_ten, binary, ternary]

        zero_to_nine = TextMobject("""
            0, 1, 2, 3, 4,
            5, 6, 7, 8, 9
        """)
        zero_to_nine.next_to(base_ten, DOWN, buff = LARGE_BUFF)
        zero_one = TextMobject("0, 1")
        zero_one.next_to(binary, DOWN, buff = LARGE_BUFF)
        zero_one_two = TextMobject("0, 1, 2")
        zero_one_two.next_to(ternary, DOWN, buff = LARGE_BUFF)
        zero_one_two.set_color_by_gradient(BLUE, GREEN)

        symbols = [zero_to_nine, zero_one, zero_one_two]
        names = ["Digits", "Bits", "Trits?"]
        for mob, text in zip(symbols, names):
            mob.brace = Brace(mob)
            mob.name = mob.brace.get_text(text)
        zero_one_two.name.set_color_by_gradient(BLUE, GREEN)
        dots = TexMobject("\\dots")
        dots.next_to(zero_one.name, RIGHT, aligned_edge = DOWN, buff = SMALL_BUFF)

        keith = Keith()
        keith.shift(2*DOWN+3*LEFT)
        keith.look_at(zero_one_two)
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)

        for title, symbol in zip(titles, symbols):
            self.play(FadeIn(title))
            added_anims = []
            if title is not ternary:
                added_anims += [
                    FadeIn(symbol.brace),
                    FadeIn(symbol.name)
                ]
            self.play(Write(symbol, run_time = 2), *added_anims)
            self.wait()
        self.play(FadeIn(keith))
        self.play(keith.change_mode, "confused")
        self.play(keith.look_at, zero_to_nine)
        self.play(keith.look_at, zero_one)
        self.play(
            GrowFromCenter(zero_one_two.brace),
            Write(zero_one_two.name),
            keith.look_at, zero_one_two,
        )
        self.play(keith.change_mode, "sassy")
        self.play(Blink(keith))
        self.play(FadeIn(morty))
        self.play(
            morty.change_mode, "sassy",
            morty.look_at, zero_one_two
        )
        self.play(Blink(morty))
        self.wait()
        self.play(
            morty.shrug,
            morty.look_at, keith.eyes,
            keith.shrug,
            keith.look_at, morty.eyes
        )
        self.wait()
        self.play(
            morty.change_mode, "hesitant",
            morty.look_at, zero_one.name,
            keith.change_mode, "erm",
            keith.look_at, zero_one.name
        )
        self.play(Blink(morty))
        self.play(Write(dots, run_time = 3))
        self.wait()

class IntroduceTernaryCounting(CountingScene):
    CONFIG = {
        "base" : 3,
        "counting_dot_starting_position" : (FRAME_X_RADIUS-1)*RIGHT + (FRAME_Y_RADIUS-1)*UP,
        "count_dot_starting_radius" : 0.5,
        "dot_configuration_height" : 1,
        "ones_configuration_location" : UP+2*RIGHT,
        "num_scale_factor" : 2,
        "num_start_location" : DOWN+RIGHT,
    }
    def construct(self):
        for x in range(2):
            self.increment()
        self.wait()
        self.increment()
        brace = Brace(self.number_mob[-1])
        threes_place = brace.get_text("Three's place")
        self.play(
            GrowFromCenter(brace),
            Write(threes_place)
        )
        self.wait()
        for x in range(6):
            self.increment()
            self.wait()
        new_brace = Brace(self.number_mob[-1])
        nines_place = new_brace.get_text("Nine's place")
        self.play(
            Transform(brace, new_brace),
            Transform(threes_place, nines_place),
        )
        self.wait()
        for x in range(9):
            self.increment()

class TernaryCountingSelfSimilarPattern(Scene):
    CONFIG = {
        "num_trits" : 3,
        "colors" : CountingScene.CONFIG["power_colors"][:3],
    }
    def construct(self):
        colors = self.colors

        title = TextMobject("Count to " + "2"*self.num_trits)
        for i, color in enumerate(colors):
            title[-i-1].set_color(color)
        steps = VGroup(*list(map(TextMobject, [
            "Count to %s,"%("2"*(self.num_trits-1)),
            "Roll over,",
            "Count to %s,"%("2"*(self.num_trits-1)),
            "Roll over,",
            "Count to %s,"%("2"*(self.num_trits-1)),
        ])))
        steps.arrange(RIGHT)
        for step in steps[::2]:
            for i, color in enumerate(colors[:-1]):
                step[-i-2].set_color(color)
        VGroup(*steps[1::2]).set_color(colors[-1])
        steps.set_width(FRAME_WIDTH-1)
        brace = Brace(steps, UP)
        word_group = VGroup(title, brace, steps)
        word_group.arrange(DOWN)
        word_group.to_edge(UP)

        ternary_mobs = VGroup(*[
            get_ternary_tex_mob(n, n_trits = self.num_trits)
            for n in range(3**self.num_trits)
        ])
        ternary_mobs.scale(2)
        ternary_mob_iter = it.cycle(ternary_mobs)
        curr_ternary_mob = next(ternary_mob_iter)

        for trits in ternary_mobs:
            trits.align_data(curr_ternary_mob)
            for trit, color in zip(trits, colors):
                trit.set_color(color)
        def get_increment():
            return Transform(
                curr_ternary_mob, next(ternary_mob_iter),
                lag_ratio = 0.5,
                path_arc = -np.pi/3
            )

        self.add(curr_ternary_mob, title)
        self.play(GrowFromCenter(brace))
        for i, step in enumerate(steps):
            self.play(Write(step, run_time = 1))
            if i%2 == 0:
                self.play(
                    Succession(*[
                        get_increment()
                        for x in range(3**(self.num_trits-1)-1)
                    ]),
                    run_time = 1
                )
            else:
                self.play(get_increment())
            self.wait()

class TernaryCountingSelfSimilarPatternFiveTrits(TernaryCountingSelfSimilarPattern):
    CONFIG = {
        "num_trits" : 5,
        "colors" : color_gradient([YELLOW, PINK, RED], 5),
    }

class CountInTernary(IntroduceTernaryCounting):
    def construct(self):
        for x in range(9):
            self.increment()
        self.wait()

class SolveConstrainedWithTernaryCounting(ConstrainedTowersOfHanoiScene):
    CONFIG = {
        "num_disks" : 4,
    }    
    def construct(self):
        for x in range(3**self.num_disks-1):
            self.increment(run_time = 0.75)
        self.wait()

    def setup(self):
        ConstrainedTowersOfHanoiScene.setup(self)
        ternary_mobs = VGroup(*[
            get_ternary_tex_mob(x)
            for x in range(3**self.num_disks)
        ])
        ternary_mobs.scale(2)
        ternary_mobs.next_to(self.peg_labels, DOWN)

        for trits in ternary_mobs:
            trits.align_data(ternary_mobs[0])
            trits.set_color_by_gradient(*self.disk_start_and_end_colors)
        self.ternary_mob_iter = it.cycle(ternary_mobs)            
        self.curr_ternary_mob = self.ternary_mob_iter.next().copy()            
        self.disk_index_iter = it.cycle(
            get_ternary_ruler_sequence(self.num_disks-1)
        )
        self.ternary_mobs = ternary_mobs

    def increment(self, run_time = 1, stay_on_peg = False):
        self.increment_number(run_time)
        self.move_next_disk(run_time, stay_on_peg)

    def increment_number(self, run_time = 1):
        self.play(Transform(
            self.curr_ternary_mob, next(self.ternary_mob_iter),
            path_arc = -np.pi/3,
            lag_ratio = 0.5, 
            run_time = run_time,
        ))

    def move_next_disk(self, run_time = None, stay_on_peg = False):
        self.move_disk(
            next(self.disk_index_iter), 
            run_time = run_time,
            stay_on_peg = stay_on_peg
        )

class DescribeSolutionByCountingToConstrained(SolveConstrainedWithTernaryCounting):
    def construct(self):
        braces = [
            Brace(VGroup(*self.curr_ternary_mob[:n+1]))
            for n in range(self.num_disks)
        ]
        words = [
            brace.get_text(text)
            for brace, text in zip(braces, [
                "Only flip last trit",
                "Roll over once",
                "Roll over twice",
                "Roll over three times",
            ])
        ]

        #Count 1, 2
        color = YELLOW
        brace = braces[0]
        word = words[0]
        words[0].set_color(color)
        self.increment_number()        
        self.play(
            FadeIn(brace),
            Write(word, run_time = 1),
            self.curr_ternary_mob[0].set_color, color
        )
        self.wait()
        self.play(
            self.disks[0].set_fill, color,
            self.disks[0].label.set_fill, BLACK,
        )
        self.move_next_disk(stay_on_peg = True)
        self.wait()
        self.ternary_mobs[2][0].set_color(color)
        self.increment_number()
        self.move_next_disk(stay_on_peg = True)
        self.wait()
        
        #Count 10
        color = MAROON_B
        words[1].set_color(color)
        self.increment_number()
        self.play(
            Transform(brace, braces[1]),
            Transform(word, words[1]),
            self.curr_ternary_mob[1].set_color, color
        )
        self.wait()
        self.play(
            self.disks[1].set_fill, color,
            self.disks[1].label.set_fill, BLACK,
        )
        self.move_next_disk(stay_on_peg = True)
        self.wait()
        self.play(*list(map(FadeOut, [brace, word])))

        #Count up to 22
        for x in range(5):
            self.increment()
            self.wait()

        #Count to 100
        color = RED
        words[2].set_color(color)

        self.wait()
        self.increment_number()
        self.play(
            FadeIn(braces[2]),
            Write(words[2], run_time = 1),
            self.curr_ternary_mob[2].set_fill, color,
            self.disks[2].set_fill, color,
            self.disks[2].label.set_fill, BLACK,
        )
        self.wait()
        self.move_next_disk(stay_on_peg = True)
        self.wait()
        self.play(*list(map(FadeOut, [braces[2], words[2]])))

        for x in range(20):
            self.increment()

class Describe2222(Scene):
    def construct(self):
        ternary_mob = TexMobject("2222").scale(1.5)
        brace = Brace(ternary_mob)
        description = brace.get_text("$3^4 - 1 = 80$")
        VGroup(ternary_mob, brace, description).scale(1.5)

        self.add(ternary_mob)
        self.wait()
        self.play(GrowFromCenter(brace))
        self.play(Write(description))
        self.wait()

class KeithAsksAboutConfigurations(Scene):
    def construct(self):
        keith = Keith().shift(2*DOWN+3*LEFT)
        morty = Mortimer().shift(2*DOWN+3*RIGHT)
        keith.make_eye_contact(morty)
        bubble = keith.get_bubble(SpeechBubble)
        bubble.write("Think about how many \\\\ configurations there are.")

        self.add(keith, morty)
        self.play(Blink(keith))
        self.play(
            keith.change_mode, "speaking",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.play(morty.change_mode, "pondering")
        self.wait()

class AskAboutConfigurations(SolveConstrainedWithTernaryCounting):
    def construct(self):
        question = TextMobject("How many configurations?")
        question.scale(1.5)
        question.to_edge(UP)
        self.add(question)

        for x in range(15):
            self.remove(self.curr_ternary_mob)
            self.wait(2)
            for y in range(7):
                self.increment(run_time = 0)

class AnswerConfigurationsCount(TowersOfHanoiScene):
    CONFIG = {
        "middle_peg_bottom" : 2.5*DOWN,
        "num_disks" : 4,
        "peg_height" : 1.5,
    }
    def construct(self):
        answer = TextMobject("$3^4 = 81$ configurations")
        answer.to_edge(UP)
        self.add(answer)

        parentheticals = self.get_parentheticals(answer)

        self.prepare_disks()

        for parens, disk in zip(parentheticals, reversed(list(self.disks))):
            VGroup(parens, parens.brace, parens.three).set_color(disk.get_color())
            self.play(
                Write(parens, run_time = 1),
                FadeIn(disk)
            )
            self.play(
                ApplyMethod(
                    disk.next_to, self.pegs[2], UP,
                    run_time = 2
                ),
                GrowFromCenter(parens.brace),
                Write(parens.three, run_time = 1)
            )
            x_diff = disk.saved_state.get_center()[0]-disk.get_center()[0]
            self.play(
                disk.shift, x_diff*RIGHT
            )
            self.play(disk.restore)
            self.wait()

    def get_parentheticals(self, top_mob):
        parentheticals = VGroup(*reversed([
            TexMobject("""
                \\left(
                    \\begin{array}{c}
                        \\text{Choices for} \\\\
                        \\text{disk %d}
                    \\end{array}
                \\right)
            """%d)
            for d in range(self.num_disks)
        ]))
        parentheticals.arrange()
        parentheticals.set_width(FRAME_WIDTH-1)
        parentheticals.next_to(top_mob, DOWN)
        for parens in parentheticals:
            brace = Brace(parens)
            three = brace.get_text("$3$")
            parens.brace = brace
            parens.three = three
        return parentheticals

    def prepare_disks(self):
        configuration = [1, 2, 1, 0]
        for n, peg_index in enumerate(configuration):
            disk_index = self.num_disks-n-1
            disk = self.disks[disk_index]
            top = Circle(radius = disk.get_width()/2)
            inner = Circle(radius = self.peg_width/2)
            inner.flip()
            top.add_subpath(inner.points)
            top.set_stroke(width = 0)
            top.set_fill(disk.get_color())
            top.rotate(np.pi/2, RIGHT)
            top.move_to(disk, UP)
            bottom = top.copy()
            bottom.move_to(disk, DOWN)
            disk.remove(disk.label)
            disk.add(top, bottom, disk.label)
            self.move_disk_to_peg(disk_index, peg_index, run_time = 0)
            if disk_index == 0:
                disk.move_to(self.pegs[peg_index].get_bottom(), DOWN)
        for disk in self.disks:
            disk.save_state()
            disk.rotate(np.pi/30, RIGHT)
            disk.next_to(self.pegs[0], UP)
        self.remove(self.disks)

class ThisIsMostEfficientText(Scene):
    def construct(self):
        text = TextMobject("This is the most efficient solution")
        text.set_width(FRAME_WIDTH - 1)
        text.to_edge(DOWN)
        self.play(Write(text))
        self.wait(2)

class RepeatingConfiguraiton(Scene):
    def construct(self):
        dots = VGroup(*[Dot() for x in range(10)])
        arrows = VGroup(*[Arrow(LEFT, RIGHT) for x in range(9)])
        arrows.add(VGroup())
        arrows.scale(0.5)
        group = VGroup(*it.chain(*list(zip(dots, arrows))))
        group.arrange()
        title = TextMobject("Same state twice")
        title.shift(3*UP)
        special_dots = VGroup(dots[2], dots[6])
        special_arrows = VGroup(*[
            Arrow(title.get_bottom(), dot, color = RED)
            for dot in special_dots
        ])
        mid_mobs = VGroup(*group[5:14])
        mid_arrow = Arrow(dots[2], dots[7], tip_length = 0.125)
        more_efficient = TextMobject("More efficient")
        more_efficient.next_to(mid_arrow, UP)

        self.play(ShowCreation(group, run_time = 2))
        self.play(Write(title))
        self.play(
            ShowCreation(special_arrows),
            special_dots.set_color, RED
        )
        self.wait()
        self.play(
            mid_mobs.shift, 2*DOWN,
            FadeOut(special_arrows)
        )
        self.play(
            ShowCreation(mid_arrow),
            Write(more_efficient)
        )
        self.wait()

class ShowSomeGraph(Scene):
    def construct(self):
        title = TextMobject("Graphs")
        title.scale(2)
        title.to_edge(UP)

        nodes = VGroup(*list(map(Dot, [
            2*LEFT, 
            UP,
            DOWN,
            2*RIGHT,
            2*RIGHT+2*UP,
            2*RIGHT+2*DOWN,
            4*RIGHT+2*UP,
        ])))
        edge_pairs = [
            (0, 1),
            (0, 2),
            (1, 3),
            (2, 3),
            (3, 4),
            (3, 5),
            (4, 6),
        ]
        edges = VGroup()
        for i, j in edge_pairs:
            edges.add(Line(
                nodes[i].get_center(), 
                nodes[j].get_center(), 
            ))

        self.play(Write(title))
        for mob in nodes, edges:
            mob.set_color_by_gradient(YELLOW, MAROON_B)
            self.play(ShowCreation(
                mob, 
                lag_ratio = 0.5,
                run_time = 2,
            ))
        self.wait()

class SierpinskiGraphScene(Scene):
    CONFIG = {
        "num_disks" : 3,
        "towers_config" : {
            "num_disks" : 3,
            "peg_height" : 1.5,
            "peg_spacing" : 2,
            "include_peg_labels" : False,
            "disk_min_width" : 1,
            "disk_max_width" : 2,
        },
        "preliminary_node_radius" : 1,
        "center_to_island_length" : 2.0,
        "include_towers" : True,
        "start_color" : RED,
        "end_color" : GREEN,
        "graph_stroke_width" : 2,
    }
    def setup(self):
        self.initialize_nodes()
        self.add(self.nodes)

        self.initialize_edges()
        self.add(self.edges)

    def initialize_nodes(self):
        circles = self.get_node_circles(self.num_disks)
        circles.set_color_by_gradient(self.start_color, self.end_color)
        circles.set_fill(BLACK, opacity = 0.7)
        circles.set_stroke(width = self.graph_stroke_width)

        self.nodes = VGroup()
        for circle in circles.get_family():
            if not isinstance(circle, Circle):
                continue
            node = VGroup()
            node.add(circle)
            node.circle = circle
            self.nodes.add(node)
        if self.include_towers:
            self.add_towers_to_nodes()
        self.nodes.set_height(FRAME_HEIGHT-2)
        self.nodes.to_edge(UP)

    def get_node_circles(self, order = 3):
        if order == 0:
            return Circle(radius = self.preliminary_node_radius)
        islands = [self.get_node_circles(order-1) for x in range(3)]
        for island, angle in (islands[0], np.pi/6), (islands[2], 5*np.pi/6):
            island.rotate(
                np.pi,
                rotate_vector(RIGHT, angle),
                about_point = island.get_center_of_mass()
            )
        for n, island in enumerate(islands):
            vect = rotate_vector(RIGHT, -5*np.pi/6-n*2*np.pi/3)
            island.scale(0.5)
            island.shift(vect)
        return VGroup(*islands)

    def add_towers_to_nodes(self):
        towers_scene = ConstrainedTowersOfHanoiScene(**self.towers_config)
        tower_scene_group = VGroup(*towers_scene.get_mobjects())
        ruler_sequence = get_ternary_ruler_sequence(self.num_disks-1)
        self.disks = VGroup(*[VGroup() for x in range(self.num_disks)])

        for disk_index, node in zip(ruler_sequence+[0], self.nodes):
            towers = tower_scene_group.copy()
            for mob in towers:
                if hasattr(mob, "label"):
                    self.disks[int(mob.label.tex_string)].add(mob)
            towers.set_width(0.85*node.get_width())
            towers.move_to(node)
            node.towers = towers
            node.add(towers)
            towers_scene.move_disk(disk_index, run_time = 0)            

    def distance_between_nodes(self, i, j):
        return get_norm(
            self.nodes[i].get_center()-\
            self.nodes[j].get_center()
        )

    def initialize_edges(self):
        edges = VGroup()
        self.edge_dict = {}
        min_distance = self.distance_between_nodes(0, 1)
        min_distance *= 1.1 ##Just a little buff to be sure
        node_radius = self.nodes[0].get_width()/2
        for i, j in it.combinations(list(range(3**self.num_disks)), 2):
            center1 = self.nodes[i].get_center()
            center2 = self.nodes[j].get_center()
            vect = center1-center2
            distance = get_norm(center1 - center2)
            if distance < min_distance:
                edge = Line(
                    center1 - (vect/distance)*node_radius,
                    center2 + (vect/distance)*node_radius,
                    color = self.nodes[i].circle.get_stroke_color(),
                    stroke_width = self.graph_stroke_width,
                )
                edges.add(edge)
                self.edge_dict[self.node_indices_to_key(i, j)] = edge
        self.edges = edges

    def node_indices_to_key(self, i, j):
        return ",".join(map(str, sorted([i, j])))

    def node_indices_to_edge(self, i, j):
        key = self.node_indices_to_key(i, j)
        if key not in self.edge_dict:
            warnings.warn("(%d, %d) is not an edge"%(i, j))
            return VGroup()
        return self.edge_dict[key]

    def zoom_into_node(self, node_index, order = 0):
        start_index = node_index - node_index%(3**order)
        node_indices = [start_index + r for r in range(3**order)]
        self.zoom_into_nodes(node_indices)

    def zoom_into_nodes(self, node_indices):
        nodes = VGroup(*[
            self.nodes[index].circle
            for index in node_indices
        ])
        everything = VGroup(*self.get_mobjects())
        if nodes.get_width()/nodes.get_height() > FRAME_X_RADIUS/FRAME_Y_RADIUS:
            scale_factor = (FRAME_WIDTH-2)/nodes.get_width()
        else:
            scale_factor = (FRAME_HEIGHT-2)/nodes.get_height()
        self.play(
            everything.shift, -nodes.get_center(),
            everything.scale, scale_factor
        )
        self.remove(everything)
        self.add(*everything)
        self.wait()

class IntroduceGraphStructure(SierpinskiGraphScene):
    CONFIG = {
        "include_towers" : True, 
        "graph_stroke_width" : 3,
        "num_disks" : 3,
    }
    def construct(self):
        self.remove(self.nodes, self.edges)
        self.introduce_nodes()
        self.define_edges()
        self.tour_structure()

    def introduce_nodes(self):
        self.play(FadeIn(
            self.nodes,
            run_time = 3,
            lag_ratio = 0.5,
        ))
        vect = LEFT
        for index in 3, 21, 8, 17, 10, 13:
            node = self.nodes[index]
            node.save_state()
            self.play(
                node.set_height, FRAME_HEIGHT-2,
                node.next_to, ORIGIN, vect
            )
            self.wait()
            self.play(node.restore)
            node.saved_state = None
            vect = -vect

    def define_edges(self):
        nodes = [self.nodes[i] for i in (12, 14)]
        for node, vect in zip(nodes, [LEFT, RIGHT]):
            node.save_state()
            node.generate_target()
            node.target.set_height(5)
            node.target.center()
            node.target.to_edge(vect)
            arc = Arc(angle = -2*np.pi/3, start_angle = 5*np.pi/6)
            if vect is RIGHT:
                arc.flip()
            arc.set_width(0.8*node.target.towers.get_width())
            arc.next_to(node.target.towers, UP)
            arc.add_tip()
            arc.set_color(YELLOW)
            node.arc = arc

        self.play(*list(map(MoveToTarget, nodes)))
        edge = Line(
            nodes[0].get_right(), nodes[1].get_left(),
            color = YELLOW,
            stroke_width = 6,
        )
        edge.target = self.node_indices_to_edge(12, 14)
        self.play(ShowCreation(edge))
        self.wait()
        for node in nodes:
            self.play(ShowCreation(node.arc))
        self.wait()
        self.play(*[
            FadeOut(node.arc)
            for node in nodes
        ])
        self.play(
            MoveToTarget(edge),
            *[node.restore for node in nodes]
        )
        self.wait()
        self.play(ShowCreation(self.edges, run_time = 3))
        self.wait()

    def tour_structure(self):
        for n in range(3):
            self.zoom_into_node(n)
        self.zoom_into_node(0, 1)
        self.play(
            self.disks[0].set_color, YELLOW,
            *[
                ApplyMethod(disk.label.set_color, BLACK)
                for disk in self.disks[0]
            ]
        )
        self.wait()
        self.zoom_into_node(0, 3)
        self.zoom_into_node(15, 1)
        self.wait()
        self.zoom_into_node(20, 1)
        self.wait()

class DescribeTriforcePattern(SierpinskiGraphScene):
    CONFIG = {
        "index_pairs" : [(7, 1), (2, 3), (5, 6)],
        "scale" : 2,
        "disk_color" : MAROON_B,
        "include_towers" : True,
        "first_connect_0_and_2_islands" : True, #Dumb that I have to do this
    }
    def construct(self):
        index_pair = self.index_pairs[0]
        self.zoom_into_node(index_pair[0], self.scale)
        self.play(
            self.disks[self.scale-1].set_color, self.disk_color,
            *[
                ApplyMethod(disk.label.set_color, BLACK)
                for disk in self.disks[self.scale-1]
            ]
        )

        nodes = [self.nodes[i] for i in index_pair]
        for node, vect in zip(nodes, [LEFT, RIGHT]):
            node.save_state()
            node.generate_target()
            node.target.set_height(6)
            node.target.center().next_to(ORIGIN, vect)

        self.play(*list(map(MoveToTarget, nodes)))
        self.wait()
        self.play(*[node.restore for node in nodes])
        bold_edges = [
            self.node_indices_to_edge(*pair).copy().set_stroke(self.disk_color, 6)
            for pair in self.index_pairs
        ]
        self.play(ShowCreation(bold_edges[0]))
        self.wait()
        self.play(*list(map(ShowCreation, bold_edges[1:])))
        self.wait()

        power_of_three = 3**(self.scale-1)
        index_sets = [
            list(range(0, power_of_three)),
            list(range(power_of_three, 2*power_of_three)),
            list(range(2*power_of_three, 3*power_of_three)),
        ]
        if self.first_connect_0_and_2_islands:
            index_sets = [index_sets[0], index_sets[2], index_sets[1]]
        islands = [
            VGroup(*[self.nodes[i] for i in index_set])
            for index_set in index_sets
        ]
        def wiggle_island(island):
            return ApplyMethod(
                island.rotate_in_place, np.pi/12, 
                run_time = 1,
                rate_func = wiggle
            )
        self.play(*list(map(wiggle_island, islands[:2])))
        self.wait()
        self.play(wiggle_island(islands[2]))
        self.wait()
        for index_set in index_sets:
            self.zoom_into_nodes(index_set)
        self.zoom_into_nodes(list(it.chain(*index_sets)))
        self.wait()

class TriforcePatternWord(Scene):
    def construct(self):
        word = TextMobject("Triforce \\\\ pattern")
        word.scale(2)
        word.to_corner(DOWN+RIGHT)
        self.play(Write(word))
        self.wait(2)

class DescribeOrderTwoPattern(DescribeTriforcePattern):
    CONFIG = {
        "index_pairs" : [(8, 9), (17, 18), (4, 22)],
        "scale" : 3,
        "disk_color" : RED,
        "first_connect_0_and_2_islands" : False,
    }

class BiggerTowers(SierpinskiGraphScene):
    CONFIG = {
        "num_disks" : 6,
        "include_towers" : False
    }
    def construct(self):
        for order in range(3, 7):
            self.zoom_into_node(0, order)

class ShowPathThroughGraph(SierpinskiGraphScene):
    CONFIG = {
        "include_towers" : True
    }
    def construct(self):
        arrows = VGroup(*[
            Arrow(
                n1.get_center(),
                n2.get_center(),
                tip_length = 0.15,
                buff = 0.15
            )
            for n1, n2 in zip(self.nodes, self.nodes[1:])
        ])
        self.wait()
        self.play(ShowCreation(
            arrows,
            rate_func=linear,
            run_time = 5
        ))
        self.wait(2)
        for index in range(9):
            self.zoom_into_node(index)

class MentionFinalAnimation(Scene):
    def construct(self):
        morty = Mortimer()
        morty.shift(2*DOWN+3*RIGHT)
        bubble = morty.get_bubble(SpeechBubble, width = 6)
        bubble.write("Before the final\\\\ animation...")

        self.add(morty)
        self.wait()
        self.play(
            morty.change_mode, "speaking",
            morty.look_at, bubble.content,
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait(2)
        self.play(Blink(morty))
        self.wait(2)

class PatreonThanks(Scene):
    CONFIG = {
        "specific_patrons" : [
            "CrypticSwarm",
            "Ali Yahya",
            "Juan    Batiz-Benet",
            "Yu  Jun",
            "Othman  Alikhan",
            "Joseph  John Cox",
            "Luc Ritchie",
            "Einar Johansen",
            "Rish    Kundalia",
            "Achille Brighton",
            "Kirk    Werklund",
            "Ripta   Pasay",
            "Felipe  Diniz",
        ]
    }
    def construct(self):
        morty = Mortimer()
        morty.next_to(ORIGIN, DOWN)

        n_patrons = len(self.specific_patrons)
        special_thanks = TextMobject("Special thanks to:")
        special_thanks.set_color(YELLOW)
        special_thanks.shift(3*UP)

        left_patrons = VGroup(*list(map(TextMobject, 
            self.specific_patrons[:n_patrons/2]
        )))
        right_patrons = VGroup(*list(map(TextMobject, 
            self.specific_patrons[n_patrons/2:]
        )))
        for patrons, vect in (left_patrons, LEFT), (right_patrons, RIGHT):
            patrons.arrange(DOWN, aligned_edge = LEFT)
            patrons.next_to(special_thanks, DOWN)
            patrons.to_edge(vect, buff = LARGE_BUFF)

        self.play(morty.change_mode, "gracious")
        self.play(Write(special_thanks, run_time = 1))
        self.play(
            Write(left_patrons),
            morty.look_at, left_patrons
        )
        self.play(
            Write(right_patrons),
            morty.look_at, right_patrons
        )
        self.play(Blink(morty))
        for patrons in left_patrons, right_patrons:
            for index in 0, -1:
                self.play(morty.look_at, patrons[index])
                self.wait()

class MortyLookingAtRectangle(Scene):
    def construct(self):
        morty = Mortimer()
        morty.to_corner(DOWN+RIGHT)
        url = TextMobject("www.desmos.com/careers")
        url.to_corner(UP+LEFT)
        rect = Rectangle(height = 9, width = 16)
        rect.set_height(5)
        rect.next_to(url, DOWN)
        rect.shift_onto_screen()
        url.save_state()
        url.next_to(morty.get_corner(UP+LEFT), UP)

        self.play(morty.change_mode, "raise_right_hand")
        self.play(Write(url))
        self.play(Blink(morty))
        self.wait()
        self.play(
            url.restore,
            morty.change_mode, "happy"
        )
        self.play(ShowCreation(rect))
        self.wait()
        self.play(Blink(morty))
        self.wait()

class ShowSierpinskiCurvesOfIncreasingOrder(Scene):
    CONFIG = {
        "sierpinski_graph_scene_config" :{
            "include_towers" : False
        },
        "min_order" : 2,
        "max_order" : 7,
        "path_stroke_width" : 7,
    }
    def construct(self):
        graph_scenes = [
            SierpinskiGraphScene(
                num_disks = order,
                **self.sierpinski_graph_scene_config
            )
            for order in range(self.min_order, self.max_order+1)
        ]
        paths = [self.get_path(scene) for scene in graph_scenes]
        graphs = []
        for scene in graph_scenes:
            graphs.append(scene.nodes)
        for graph in graphs:
            graph.set_fill(opacity = 0)

        graph, path = graphs[0], paths[0]

        self.add(graph)
        self.wait()
        self.play(ShowCreation(path, run_time = 3, rate_func=linear))
        self.wait()   
        self.play(graph.fade, 0.5, Animation(path))
        for other_graph in graphs[1:]:
            other_graph.fade(0.5)
        self.wait()
        for new_graph, new_path in zip(graphs[1:], paths[1:]):
            self.play(
                Transform(graph, new_graph),
                Transform(path, new_path),
                run_time = 2
            )
            self.wait()
        self.path = path

    def get_path(self, graph_scene):
        path = VGroup()
        nodes = graph_scene.nodes
        for n1, n2, n3 in zip(nodes, nodes[1:], nodes[2:]):
            segment = VMobject()
            segment.set_points_as_corners([
                n1.get_center(),
                n2.get_center(),
                n3.get_center(),
            ])
            path.add(segment)
        path.set_color_by_gradient(
            graph_scene.start_color,
            graph_scene.end_color,
        )
        path.set_stroke(
            width = self.path_stroke_width - graph_scene.num_disks/2
        )
        return path

class Part1Thumbnail(Scene):
    CONFIG = {
        "part_number" : 1,
        "sierpinski_order" : 5
    }
    def construct(self):
        toh_scene = TowersOfHanoiScene(
            peg_spacing = 2,
            part_number = 1,
        )
        toh_scene.remove(toh_scene.peg_labels)
        toh_scene.pegs[2].set_fill(opacity = 0.5)
        toh = VGroup(*toh_scene.get_mobjects())
        toh.scale(2)
        toh.to_edge(DOWN)
        self.add(toh)

        sierpinski_scene = ShowSierpinskiCurvesOfIncreasingOrder(
            min_order = self.sierpinski_order,
            max_order = self.sierpinski_order,
            skip_animations = True,
        )
        sierpinski_scene.path.set_stroke(width = 10)        
        sierpinski = VGroup(*sierpinski_scene.get_mobjects())
        sierpinski.scale(0.9)
        sierpinski.to_corner(DOWN+RIGHT)
        self.add(sierpinski)

        binary = TexMobject("01011")
        binary.set_color_by_tex("0", GREEN)
        binary.set_color_by_tex("1", BLUE)
        binary.set_color_by_gradient(GREEN, RED)
        binary.add_background_rectangle()
        binary.background_rectangle.set_fill(opacity = 0.5)
        # binary.set_fill(opacity = 0.5)
        binary.scale(4)
        binary.to_corner(UP+LEFT)
        self.add(binary)

        part = TextMobject("Part %d"%self.part_number)
        part.scale(4)
        part.to_corner(UP+RIGHT)
        part.add_background_rectangle()
        self.add(part)

class Part2Thumbnail(Part1Thumbnail):
    CONFIG = {
        "part_number" : 2
    }


















