Skip to content

Instantly share code, notes, and snippets.

@0xpizza
Created October 7, 2020 23:15
Show Gist options
  • Select an option

  • Save 0xpizza/35864bcec8a6f61d947ddf148af07c8a to your computer and use it in GitHub Desktop.

Select an option

Save 0xpizza/35864bcec8a6f61d947ddf148af07c8a to your computer and use it in GitHub Desktop.
#!python3.8
import math
import random
import secrets
import itertools
import threading
import functools
from types import SimpleNamespace
from tkinter import *
from tkinter.scrolledtext import ScrolledText
def gen_rainbow(domain=None):
"""This generator uses the sine function to derive a
cyclic sequence of numbers between 0 and 1. These numbers
are used as percentages of 0xFF -- the maximum value
of an 8-bit RGB code. By default, this generator yields 50
RGB codes, but can yeild any number depending on the interval
of the domain.
"""
if domain is None:
domain = itertools.cycle(x/50 for x in range(50)) # interval = 0.2
for _ in range(secrets.randbelow(50)):
next(domain) # advance ? cycles for a random start color
while True:
x = next(domain)
RGB = [
math.sin(x * 2 * math.pi) / 2 + 0.5,
math.sin(x * 2 * math.pi + 2 * math.pi/3) / 2 + 0.5,
math.sin(x * 2 * math.pi + 4 * math.pi / 3) / 2 + 0.5,
]
yield '#' + bytes(int(y*255) for y in RGB).hex()
class Die(SimpleNamespace):
@classmethod
def roll(cls, nsides=6, **kwargs):
"""Returns a Die object with its value, the number
of sides the die had, and any metadata kwargs supplied
by the caller of the method
"""
result = secrets.randbelow(nsides)+1
return cls(value=result, range=range(1, nsides+1), **kwargs)
def __str__(self):
return str(self.value) + ('!' if self.value == self.max else '')
@property
def min(self):
return self.range[0]
@property
def max(self):
return self.range[-1]
class ReadOnlyText(ScrolledText):
"""Special scrolled text class that can only be
modifed programatically and rejects user input.
"""
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.config(state=DISABLED)
self.insert = self._unlock(super().insert)
self.delete = self._unlock(super().delete)
def _unlock(self, f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
nonlocal self
self.config(state=NORMAL)
ret = f(*args, **kwargs)
self.config(state=DISABLED)
return ret
return wrapped
class BlackFrame(Frame):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.configure(bg='black')
class RollingDie(Frame):
"""Show a buncha random numbers, then slowly
approach the die number, then changes from
white to the die.color property. Very shiny.
"""
die_font = 'courier 40 bold'
longest_delay = 200 #ms
def __init__(self, master=None, **kwargs):
self.die = kwargs.pop('die')
super().__init__(master, **kwargs)
# to change the die's border color we change the color of self
self.config(height=30, width=30, background='white')
self.total_bounces = random.randint(20,30)
self.bounce_count = 0
# this is the number's black background
self.inner_frame = BlackFrame(self)
self.inner_frame.pack(padx=1, pady=1, fill=BOTH, expand=YES)
self.die_label = Label(
self.inner_frame, text='', foreground='white',
background='black', font=self.die_font,
)
self.die_label.pack(anchor=CENTER)
self.advance_state()
def advance_state(self):
if self.bounce_count < self.total_bounces:
self.die_label.configure(text=random.randint(self.die.min, self.die.max))
self.bounce_count += random.randint(1,3)
delay = int(self.bounce_count / self.total_bounces * self.longest_delay)
self.after(delay, self.advance_state)
else:
self.configure(background=self.die.color)
self.die_label.config(
text=str(self.die.value), foreground=self.die.color
)
class DiceRoller(ReadOnlyText):
"""Scrolled textbox filled with animated labels"""
def __init__(self, master=None, **kwargs):
self.nsides = kwargs.pop('sidevar', None) # IntVar
super().__init__(master, **kwargs)
self.hide_vbar()
self.configure(bg='black', borderwidth=0)
# when there is no sidevar, make a default var
if self.nsides is None:
self.nsides = IntVar(self)
self.nsides.set(6)
self.color = gen_rainbow()
self.bind('<Return>', self.roll)
self.bind('<space>', self.roll)
self.focus_set()
def roll(self, _event=None):
"""First check to see if we used this color already.
If not, then create a new tag. Start the roll animation.
"""
die = Die.roll(nsides=self.nsides.get(), color=next(self.color))
self.window_create(INSERT, window=RollingDie(die=die), padx=5, pady=5)
self.see(INSERT)
def hide_vbar(self):
self.vbar.pack_forget()
def show_vbar(self):
self.vbar.pack(side=LEFT, fill=BOTH, expand=True)
class ScrollableSpinbox(Spinbox):
"""https://stackoverflow.com/questions/38329996/enable-mouse-wheel-in-spinbox-tk-python
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bind('<MouseWheel>', self.mouseWheel)
self.bind('<Button-4>', self.mouseWheel)
self.bind('<Button-5>', self.mouseWheel)
def mouseWheel(self, event):
if event.num == 5 or event.delta == -120:
self.invoke('buttondown')
elif event.num == 4 or event.delta == 120:
self.invoke('buttonup')
class App(BlackFrame):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.sidevar = IntVar(self)
self.sidevar.set(6)
# create spinbox for controlling die sides
BlackFrame(self).pack(fill=X) # Horizontal spacer
f = BlackFrame(self)
f.pack(pady=10)
Label(
f, text='Sides:', foreground='white', background='black'
).pack(padx=5, side=LEFT, anchor=CENTER)
s = ScrollableSpinbox(
f, textvariable=self.sidevar, from_=1, to=99999999,
background='black', foreground='white',
)
s.pack(side=LEFT)
BlackFrame(self).pack(fill=X) # Horizontal spacer
# dice screen
f = Frame(self, background='white')
f.pack(expand=YES, fill=BOTH, padx=10, pady=10)
DiceRoller(f, borderwidth=0, sidevar=self.sidevar)\
.pack(expand=YES, fill=BOTH, padx=1, pady=1)
class Root(Tk):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.title('2D Dice')
App(self).pack(expand=YES, fill=BOTH)
def main():
Root().mainloop()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment