TUI OpenSoundControl finite state machine in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

118 lines
3.2KB

  1. import curses
  2. import random
  3. import sys
  4. import re
  5. ESCAPE = 27
  6. LETTERS = range(97, 122)
  7. NUMBERS = range(48, 57)
  8. class CheekyFSM(object):
  9. def __init__(self):
  10. self.lastkey = -1
  11. self.buffer = ""
  12. self.message = ""
  13. self.edges = {}
  14. self.state = "A"
  15. self.history = " "*30
  16. def show_edges(self):
  17. """ Print the help menu """
  18. y, x = self.w.getmaxyx()
  19. for index, ((start, end), prob) in enumerate(self.edges.items()):
  20. self.w.addstr(10 + index, 2, f"{start} -> {end} : {prob}%")
  21. def update(self):
  22. """ Update the FSM """
  23. edges = list(self.edges.items())
  24. random.shuffle(edges)
  25. for ((start, end), prob) in edges:
  26. if start == self.state:
  27. if random.randint(0, 100) < prob:
  28. self.state = end
  29. self.history = self.history[1:] + self.state
  30. def show_state(self):
  31. """ Show the state of the machine """
  32. self.w.addstr(2, 2, f"Tempo: 120bpm")
  33. self.w.addstr(4, 2, f"State: {self.state}")
  34. self.w.addstr(5, 2, f"History: {self.history}")
  35. offset = 6
  36. for ((start, end), prob) in self.edges.items():
  37. if start == self.state:
  38. self.w.addstr(offset, 2,
  39. f"Going to {end} with probability {prob}%")
  40. offset += 1
  41. def show_status(self):
  42. """ Print the status bar """
  43. y, x = self.w.getmaxyx()
  44. self.w.addstr(y - 2, 2, f"> {self.buffer}", curses.A_BOLD)
  45. def quit(self):
  46. """ Quit the program """
  47. curses.nocbreak()
  48. self.w.keypad(False)
  49. curses.echo()
  50. curses.endwin()
  51. sys.exit(0)
  52. def process_key(self):
  53. """ Process a key """
  54. key = self.w.getch()
  55. self.lastkey = key
  56. if key == ESCAPE:
  57. quit(self.w)
  58. elif key in LETTERS:
  59. self.buffer += chr(key).upper()
  60. elif key in NUMBERS:
  61. self.buffer += chr(key)
  62. elif key == 10:
  63. self.accept()
  64. elif key == 263 and len(self.buffer) > 0:
  65. self.buffer = self.buffer[:-1]
  66. def accept(self):
  67. """ Accept the string """
  68. match = re.match(r"([A-Z])([A-Z])(\d+)", self.buffer)
  69. if not match:
  70. self.message = " Invalid command :("
  71. else:
  72. groups = match.groups()
  73. start = groups[0]
  74. end = groups[1]
  75. prob = int(groups[2])
  76. self.modify(start, end, prob)
  77. self.buffer = ""
  78. def modify(self, start, end, prob):
  79. """ Make a modification """
  80. key = (start, end)
  81. self.edges[key] = prob
  82. self.message = f" {start} -> {end} : {prob}%"
  83. if prob == 0 and key in self.edges:
  84. self.message = f" {start} -> {end} : deleted"
  85. del self.edges[key]
  86. def loop(self, w):
  87. """ Run the loop """
  88. self.w = w
  89. curses.curs_set(0) # invisible
  90. w.timeout(100)
  91. while True:
  92. w.clear()
  93. self.show_edges()
  94. self.show_status()
  95. self.show_state()
  96. self.update()
  97. w.refresh()
  98. self.process_key()
  99. if __name__ == "__main__":
  100. fsm = CheekyFSM()
  101. curses.wrapper(fsm.loop)