woods_runner.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import functools
  2. from state.devstate import DevState
  3. from bootstrap.scd import bootstrap_scd
  4. from framework.conformance import Conformance, render_conformance_check_result
  5. from concrete_syntax.textual_od import parser, renderer
  6. from concrete_syntax.plantuml import renderer as plantuml
  7. from api.od import ODAPI
  8. from examples.semantics.operational.simulator import Simulator, RandomDecisionMaker, InteractiveDecisionMaker, make_actions_pure, filter_valid_actions
  9. state = DevState()
  10. scd_mmm = bootstrap_scd(state) # Load meta-meta-model
  11. ### Load (meta-)models ###
  12. # Design meta-model
  13. woods_mm_cs = """
  14. Animal:Class {
  15. abstract = True;
  16. }
  17. Bear:Class
  18. :Inheritance (Bear -> Animal)
  19. Man:Class {
  20. lower_cardinality = 1;
  21. upper_cardinality = 2;
  22. constraint = `get_value(get_slot(this, "weight")) > 20`;
  23. }
  24. :Inheritance (Man -> Animal)
  25. Man_weight:AttributeLink (Man -> Integer) {
  26. name = "weight";
  27. optional = False;
  28. }
  29. afraidOf:Association (Man -> Animal) {
  30. source_upper_cardinality = 6;
  31. target_lower_cardinality = 1;
  32. }
  33. """
  34. # Runtime meta-model
  35. woods_rt_mm_cs = woods_mm_cs + """
  36. AnimalState:Class {
  37. abstract = True;
  38. }
  39. AnimalState_dead:AttributeLink (AnimalState -> Boolean) {
  40. name = "dead";
  41. optional = False;
  42. }
  43. of:Association (AnimalState -> Animal) {
  44. source_lower_cardinality = 1;
  45. source_upper_cardinality = 1;
  46. target_lower_cardinality = 1;
  47. target_upper_cardinality = 1;
  48. }
  49. BearState:Class {
  50. constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Bear"`;
  51. }
  52. :Inheritance (BearState -> AnimalState)
  53. BearState_hunger:AttributeLink (BearState -> Integer) {
  54. name = "hunger";
  55. optional = False;
  56. constraint = ```
  57. val = get_value(get_target(this))
  58. val >= 0 and val <= 100
  59. ```;
  60. }
  61. ManState:Class {
  62. constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Man"`;
  63. }
  64. :Inheritance (ManState -> AnimalState)
  65. attacking:Association (AnimalState -> ManState) {
  66. # Animal can only attack one Man at a time
  67. target_upper_cardinality = 1;
  68. # Man can only be attacked by one Animal at a time
  69. source_upper_cardinality = 1;
  70. constraint = ```
  71. attacker = get_source(this)
  72. if get_type_name(attacker) == "BearState":
  73. # only BearState has 'hunger' attribute
  74. hunger = get_value(get_slot(attacker, "hunger"))
  75. else:
  76. hunger = 100 # Man can always attack
  77. attacker_dead = get_value(get_slot(attacker, "dead"))
  78. attacked_state = get_target(this)
  79. attacked_dead = get_value(get_slot(attacked_state, "dead"))
  80. (
  81. hunger >= 50
  82. and not attacker_dead # cannot attack while dead
  83. and not attacked_dead # cannot attack whoever is dead
  84. )
  85. ```;
  86. }
  87. attacking_starttime:AttributeLink (attacking -> Integer) {
  88. name = "starttime";
  89. optional = False;
  90. constraint = ```
  91. val = get_value(get_target(this))
  92. _, clock = get_all_instances("Clock")[0]
  93. current_time = get_slot_value(clock, "time")
  94. val >= 0 and val <= current_time
  95. ```;
  96. }
  97. # Just a clock singleton for keeping the time
  98. Clock:Class {
  99. lower_cardinality = 1;
  100. upper_cardinality = 1;
  101. }
  102. Clock_time:AttributeLink (Clock -> Integer) {
  103. name = "time";
  104. optional = False;
  105. constraint = `get_value(get_target(this)) >= 0`;
  106. }
  107. """
  108. # Our design model - the part that doesn't change
  109. woods_m_cs = """
  110. george:Man {
  111. weight = 80;
  112. }
  113. bill:Man {
  114. weight = 70;
  115. }
  116. teddy:Bear
  117. mrBrown:Bear
  118. # george is afraid of both bears
  119. :afraidOf (george -> teddy)
  120. :afraidOf (george -> mrBrown)
  121. # the men are afraid of each other
  122. :afraidOf (bill -> george)
  123. :afraidOf (george -> bill)
  124. """
  125. # Our runtime model - the part that changes with every execution step
  126. woods_rt_initial_m_cs = woods_m_cs + """
  127. georgeState:ManState {
  128. dead = False;
  129. }
  130. :of (georgeState -> george)
  131. billState:ManState {
  132. dead = False;
  133. }
  134. :of (billState -> bill)
  135. teddyState:BearState {
  136. dead = False;
  137. hunger = 40;
  138. }
  139. :of (teddyState -> teddy)
  140. mrBrownState:BearState {
  141. dead = False;
  142. hunger = 80;
  143. }
  144. :of (mrBrownState -> mrBrown)
  145. clock:Clock {
  146. time = 0;
  147. }
  148. """
  149. def parse_and_check(m_cs: str, mm, descr: str):
  150. m = parser.parse_od(
  151. state,
  152. m_text=m_cs,
  153. mm=mm)
  154. conf = Conformance(state, m, mm)
  155. print(descr, "...", render_conformance_check_result(conf.check_nominal()))
  156. return m
  157. woods_mm = parse_and_check(woods_mm_cs, scd_mmm, "MM")
  158. woods_rt_mm = parse_and_check(woods_rt_mm_cs, scd_mmm, "RT-MM")
  159. woods_m = parse_and_check(woods_m_cs, woods_mm, "M")
  160. woods_rt_m = parse_and_check(woods_rt_initial_m_cs, woods_rt_mm, "RT-M")
  161. print()
  162. ### Semantics ###
  163. # Helpers
  164. def state_of(od, animal):
  165. return od.get_source(od.get_incoming(animal, "of")[0])
  166. def animal_of(od, state):
  167. return od.get_target(od.get_outgoing(state, "of")[0])
  168. def get_time(od):
  169. _, clock = od.get_all_instances("Clock")[0]
  170. return clock, od.get_slot_value(clock, "time")
  171. # Action: Time advances, whoever is being attacked dies, bears become hungrier
  172. def action_advance_time(od):
  173. msgs = []
  174. clock, old_time = get_time(od)
  175. new_time = old_time + 1
  176. od.set_slot_value(clock, "time", new_time)
  177. for _, attacking_link in od.get_all_instances("attacking"):
  178. man_state = od.get_target(attacking_link)
  179. animal_state = od.get_source(attacking_link)
  180. if od.get_type_name(animal_state) == "BearState":
  181. od.set_slot_value(animal_state, "hunger", max(od.get_slot_value(animal_state, "hunger") - 50, 0))
  182. od.set_slot_value(man_state, "dead", True)
  183. od.delete(attacking_link)
  184. msgs.append(f"{od.get_name(animal_of(od, animal_state))} kills {od.get_name(animal_of(od, man_state))}.")
  185. for _, bear_state in od.get_all_instances("BearState"):
  186. if od.get_slot_value(bear_state, "dead"):
  187. continue # bear already dead
  188. old_hunger = od.get_slot_value(bear_state, "hunger")
  189. new_hunger = min(old_hunger + 10, 100)
  190. od.set_slot_value(bear_state, "hunger", new_hunger)
  191. bear = od.get_target(od.get_outgoing(bear_state, "of")[0])
  192. bear_name = od.get_name(bear)
  193. if new_hunger == 100:
  194. od.set_slot_value(bear_state, "dead", True)
  195. msgs.append(f"Bear {bear_name} dies of hunger.")
  196. else:
  197. msgs.append(f"Bear {bear_name}'s hunger level is now {new_hunger}.")
  198. return msgs
  199. # Action: Animal attacks Man
  200. # Note: We must use the names of the objects as parameters, because when cloning, the IDs of objects change!
  201. def action_attack(od, animal_name: str, man_name: str):
  202. msgs = []
  203. animal = od.get(animal_name)
  204. man = od.get(man_name)
  205. animal_state = state_of(od, animal)
  206. man_state = state_of(od, man)
  207. attack_link = od.create_link(None, # auto-generate link name
  208. "attacking", animal_state, man_state)
  209. _, clock = od.get_all_instances("Clock")[0]
  210. current_time = od.get_slot_value(clock, "time")
  211. od.set_slot_value(attack_link, "starttime", current_time)
  212. msgs.append(f"{animal_name} is now attacking {man_name}")
  213. return msgs
  214. # Get all actions that can be performed (including those that bring us to a non-conforming state)
  215. def get_all_actions(od):
  216. def _generate_actions(od):
  217. # can always advance time:
  218. yield ("advance time", action_advance_time)
  219. # if A is afraid of B, then B can attack A:
  220. for _, afraid_link in od.get_all_instances("afraidOf"):
  221. man = od.get_source(afraid_link)
  222. animal = od.get_target(afraid_link)
  223. animal_name = od.get_name(animal)
  224. man_name = od.get_name(man)
  225. man_state = state_of(od, man)
  226. animal_state = state_of(od, animal)
  227. descr = f"{animal_name} ({od.get_type_name(animal)}) attacks {man_name} ({od.get_type_name(man)})"
  228. yield (descr, functools.partial(action_attack, animal_name=animal_name, man_name=man_name))
  229. return make_actions_pure(_generate_actions(od), od)
  230. # Only get those actions that bring us to a conforming state
  231. def get_valid_actions(od):
  232. return filter_valid_actions(get_all_actions(od))
  233. # Render our run-time state to a string
  234. def render_woods(od):
  235. txt = ""
  236. _, time = get_time(od)
  237. txt += f"T = {time}.\n"
  238. txt += "Bears:\n"
  239. def render_attacking(animal_state):
  240. attacking = od.get_outgoing(animal_state, "attacking")
  241. if len(attacking) == 1:
  242. whom_state = od.get_target(attacking[0])
  243. whom_name = od.get_name(animal_of(od, whom_state))
  244. return f" attacking {whom_name}"
  245. else:
  246. return ""
  247. def render_dead(animal_state):
  248. return 'dead' if od.get_slot_value(animal_state, 'dead') else 'alive'
  249. for _, bear_state in od.get_all_instances("BearState"):
  250. bear = animal_of(od, bear_state)
  251. hunger = od.get_slot_value(bear_state, "hunger")
  252. txt += f" 🐻 {od.get_name(bear)} (hunger: {hunger}, {render_dead(bear_state)}) {render_attacking(bear_state)}\n"
  253. txt += "Men:\n"
  254. for _, man_state in od.get_all_instances("ManState"):
  255. man = animal_of(od, man_state)
  256. attacked_by = od.get_incoming(man_state, "attacking")
  257. if len(attacked_by) == 1:
  258. whom_state = od.get_source(attacked_by[0])
  259. whom_name = od.get_name(animal_of(od, whom_state))
  260. being_attacked = f" being attacked by {whom_name}"
  261. else:
  262. being_attacked = ""
  263. txt += f" 👨 {od.get_name(man)} ({render_dead(man_state)}) {render_attacking(man_state)}{being_attacked}\n"
  264. return txt
  265. # When should simulation stop?
  266. def termination_condition(od):
  267. _, time = get_time(od)
  268. if time >= 10:
  269. return "Took too long"
  270. # End simulation when 2 animals are dead
  271. who_is_dead = []
  272. for _, animal_state in od.get_all_instances("AnimalState"):
  273. if od.get_slot_value(animal_state, "dead"):
  274. animal_name = od.get_name(animal_of(od, animal_state))
  275. who_is_dead.append(animal_name)
  276. if len(who_is_dead) >= 2:
  277. return f"{' and '.join(who_is_dead)} are dead"
  278. sim = Simulator(
  279. action_generator=get_valid_actions,
  280. # action_generator=get_all_actions,
  281. decision_maker=RandomDecisionMaker(seed=0),
  282. # decision_maker=InteractiveDecisionMaker(),
  283. termination_condition=termination_condition,
  284. check_conformance=False,
  285. verbose=True,
  286. renderer=render_woods,
  287. )
  288. od = ODAPI(state, woods_rt_m, woods_rt_mm)
  289. sim.run(od)