Žoga.nje

Testi

Testi: testi-zoganje.py

Naloga

Za domačo nalogo bo potrebno narediti tole:

Ogrevanje: Razred

Napiši razred Ball. Konstruktor naj sprejme od 0 do 6 argumentov. Klic b = Ball(x, y, r, vx, vy, barva) naj postavi krogle s polmerom r na koordinate (x, y), smer gibanja žoge naj bo (vx, vy) in žoga naj bo barve barva. (Z gibanjem žoge se še ne ukvarjaj, to pride pri oceni 6. Za zdaj je pomembno samo, da je vektor (vx, vy) shranjen.)

Funkcija naj ima privzete argumente.

  • Če uporabnik ne poda polmera, naj bo žoga velika med 15 in 30 točk.
  • Če uporabnik ne poda koordinat x in y, izberi naključne koordinate, vendar takšne, da bo celotna žoga na sliki.
  • Če uporabnik ne poda hitrosti, stori tole: najprej naključno določi celotno hitrost žoge, ki naj bo med 5 in 10. Nato naključno določi hitrost v smeri x (vx) in potem izračunaj, kakšna je tedaj hitrost v smeri y.
  • Če uporabnik ne poda barve, izberi naključno barvo.

Nasvet: privzeta vrednost vseh argumentov naj bo None, torej x=None, y=None, r=None, vx=None, vy=None, barva=None. V funkciji nato pišeš if r is None. (Enako dobro deluje tudi if r == None, vendar je pri None navada, da se uporablja is namesto ==.) Koordinati oz. hitrost določaj, čim manjka eden od argumentov (x ali y, oziroma vx ali vy).

Če koordinate, velikost in hitrost poda ta, ki kliče funkcijo, ti ni potrebno preverjati ali so vrednosti legalne (torej, ali je krog na platno, ali je primerno velik in primerno hiter). Upoštevaj, kar poda klicatelj.

Ob klicu konstruktorja naj se ustvari objekt, poleg tega pa naj se na zasilnem platnu na ustreznem mestu nariše ustrezno velika žoga ustrezne barve. (Seveda pa se za zdaj še ne bo premikala.)

Funkcija risar.krog ima po novem še dodaten argument, zapolni. Če ga postavimo na True, izriše zapolnjen krog.

Razred naj ima tudi metode get_x, get_y, get_r, get_vx in get_vy, ki vračajo koordinate žoge, njen polmer in njeno hitrost, kakor so bili nastavljeni v konstruktorju. Poleg tega naj ima metodo get_shape, ki vrača objekt s platna -- torej tisto, kar je vrnila funkcija risar.krog; očitno bo morala imeta žoga (objekt tipa Ball) shranjeno tudi to, ne le koordinat in podobnega. Našete metode naj nimajo argumentov (razen, seveda, self).

Za preverjanje pravilnosti nujno uporabite teste. Poleg teh si lahko tudi malo narišete, kaj dobivate. Če napišete

balls = [] for i in range(1, 10): balls.append(Ball(i*40, i*40, i*4)) se mora izrisati nekaj takšnega (le z drugimi barvami):
Če napišete balls = [] for i in range(1, 10): balls.append(Ball()) pa bodo žoge naključno razmetane, recimo tako.

Ocena 6: Premik

Dodajte metodo move(), ki spremeni koordinate žoge tako, da x in y poveča za vx in vy. Metoda nima argumentov (razen, seveda self) in ne vrača ničesar.

Ocena 7: Stene

Dodajte metodo walls(), ki spremeni hitrost žoge tako, da se odbije od roba (levega, desnega, zgornjega ali spodnjega), ki se ga dotika. Pri tem ne upoštevajte le koordinat žoge temveč tudi njen polmer.

Odboj od roba navadno naredimo tako, da obrnemo smer ustrezne komponente vektorja, npr. spremenimo vx v -vx. Tu bomo delali nekoliko drugače: če se žoga dotakne leve stene, bomo poskrbeli, da bo vx pozitiven - žoga mora iti v desno. Če se dotakne desne stene, bomo poskrbeli, da bo negativen. Torej, če ima žoga vx enak -5 in se dotakne leve stene, mora imeti po tem vx enak 5. Tudi, če ima slučajno vx enak 5 in se dotakne leve stene (kako je to možno? zgodilo se bo zaradi trkov med žogami), mora imeti po tem vx enak 5. Enako velja za druge stene.

Metoda nima argumentov (razen, seveda self) in ne vrača ničesar.

Ocena 8: Razporejanje

Dodajte metodo touches(self, other), ki pove (tako, da vrne True ali False, ali se žoga (self) dotika druge žoge (other). Tudi prekrivanje se šteje za dotik. Kot ve vsak nogometaš, se dve žogi se dotikata, če je razdalja med njima manjša od vsote njunih polmerov. Valjda.

Poleg tega dodajte metodo intersects(self, balls), ki kot argument sprejme seznam žog in pove, ali se žoga (self) dotika katerekoli žoge s seznama. Pri tem naj kliče metodo touches.

Poleg tega spremenite konstruktor tako, da mu dodate še en argument. Njegovo ime naj bo existing, privzeta vrednost pa []. S tem seznamom lahko ob klicu konstruktorja podamo seznam obstoječih žog in ko konstruktor žreba koordinate nove žoge, mora paziti, da se za ne dotika (ali prekriva) s katero od obstoječih žog. Pri tem seveda uporabite metod intersects.

Končno, dodajte funkcijo (ne metode razreda Ball, temveč samostojno funkcijo) setup(n), ki vrne seznam n žog (objektov tipa Ball), na naključnih mestih, naključnih velikosti in barv, ki se med seboj ne dotikajo.

Klic setup(42), recimo, mora narediti tole:

Ocena 9: Odboj od žoge

Napišite funkcijo deflect_from(self, other), ki spremeni hitrost žog (self in other) zaradi odboja. Spremeni naj samo self.vx in self.vy ter other.vx in other.vy, pozicije žog pa pusti pri miru.

Kako se žoge odbijajo? Približno takole bo kar dobro:

Imejmo žogi V in U, njuni hitrosti označimo z v in u. Hitrost razstavimo v dve komponenti - tisto, ki gre v smeri vzporedni s središčema žog in tisto, ki je pravokotna na to smer. Označili ju bomo z \(v_n\) in \(v_p\), se pravi, \(v = v_p + v_n\). Podobno naredimo s hitrostjo druge žoge. Ob trku žogi ohranita svojo hitrost v pravokotni smeri, a izmenjata hitrosti v vzporedni smeri, torej \(v' = u_p + v_n\) in \(u' = v_p + u_n\), pri čemer sem z \(v'\) in \(u'\) označil hitrosti po trku.

Kako razstaviti hitrost na komponenti? No, to že ni več fizika temveč matematika in to bi definitivno morali znati. V pomoč pa: izračunajte vektor, ki vodi od središča žoge V do središča žoge U. Normirajte ga tako, da bo dolg 1 (se pravi, delite ga z njegovo dolžino. Če označimo takšen vektor z \(n\), potem je \(v_p\) enak \(n(n\circ v)\). Koliko je \(v_n\) pa bi se morda dalo izračunati iz \(v\) in \(v_p\).

Pozor: Besedilo naloge je spremenjeno, ker fizika ne deluje ravno tako, kot sem razlagal na predavanjih. Tudi testni primeri so pobrisani..

Ocena 10: Žoganje

Napiši program, ki s funkcijo setup() postavi 10 žog, nato pa jih do sodnega dne odbija po ekranu tako, kot kaže gornji video.

Za to bo potrebno v neskončni zanki klicati metode move, walls in deflect_from (ki bi si zdaj zaslužila ime collision -- preimenujte po želji). Pri tem pazite, da collision pokličete za vsak par žog le enkrat. Če napišete kaj v slogu

for b1 in balls: for b2 in balls: b1.collision(b2) bo to delovalo izjemno neprepričljivo.

Rešitev

Za oceno 5

def __init__(self, x=None, y=None, r=None, vx=None, vy=None, barva=None): if r is None: r = randint(15, 30) self.r = r if x is None: x = randint(self.r, risar.maxX - self.r) if y is None: y = randint(self.r, risar.maxY - self.r) self.x = x self.y = y if vx is None or vy is None: v = randint(5, 10) vx = randint(-v, v) vy = sqrt(v**2 - vx**2) * random.choice[-1, 1] self.vx = vx self.vy = vy if barva is None: barva = risar.barva(randint(0, 255), randint(0, 255), randint(0, 255)) self.krog = risar.krog(self.x, self.y, self.r, barva, 5, zapolni=True)

Kot sem predlagal na predavanjih, najprej določimo radij - če ga ta, ki kliče funkcijo, ni podal. Potrebujemo ga namreč zato, da lahko žrebamo koordinati x in y, če je potrebno. Hitrost določimo, recimo, tako, da si najprej izmislimo celotno hitrost, nato hitrost v smeri x (vx), preostanek hitrosti pa (po Pitagori) v smeri y. Pri tem hitrost v smeri y pomnožimo z -1 ali +1, da bo včasih obrnjena gor in včasih dol. Končno si po potrebi izmislimo še barvo in narišemo krog ter si ga zapomnimo.

Žoga ima tako atribute x, y, r, vx, vy in krog. Celotne hitrosti, v, si ne zapomnimo, saj je ne bomo nikoli potrebovali. Barve pa tudi ne.

Naloga je zahtevala še nekaj preprostih metod:

def get_x(self): return self.x def get_y(self): return self.y def get_vx(self): return self.vx def get_vy(self): return self.vy def get_r(self): return self.r def get_shape(self): return self.krog

Za oceno 6

V metodi move moramo premakniti žogo tako, da spremenimo self.x in self.y, poleg tega pa je potrebno premakniti še njeno sliko, tako da kličemo setPos z novimi koordinatami.

def move(self): self.x += self.vx self.y += self.vy self.krog.setPos(self.x, self.y)

Za oceno 7

Nalogo za oceno 7 bi lahko skoraj prepisali iz zapiskov predavanj, če ne bi bilo zoprnega opozorila, da mora imeti žoga po trku v levo steno vedno pozitivno hitrost (in podobno za ostale stene). Zaradi tega moramo obravnavati vsako steno posebej in računati z absolutnimi vrednostmi.

def walls(self): if self.x < self.r: self.vx = abs(self.vx) if self.x > risar.maxX - self.r: self.vx = -abs(self.vx) if self.y < self.r: self.vy = abs(self.vy) if self.y > risar.maxY - self.r: self.vy = -abs(self.vy)

Za oceno 8

Metoda touches je trivialna, intersects pa deluje tako kot sto in ena podobna funkcija, ki smo jo že napisali, začenši z iskanjem praštevil.

def touches(self, other): return (self.x - other.x) ** 2 + (self.y - other.y) ** 2 <= (self.r + other.r) ** 2 def intersects(self, balls): for ball in balls: if self.touches(ball): return True return False

Poleg tega moramo pri nalogi za oceno 8 dodati konstruktorju argument existing, katerega privzeta vrednost je prazen seznam

def __init__(self, x=None, y=None, r=None, vx=None, vy=None, barva=None, existing=[]):

in poskrbeti, da pri žrebanju koordinat ne prekrijemo obstoječega kroga:

if x is None or y is None: while True: self.x = randint(self.r, risar.maxX - self.r) self.y = randint(self.r, risar.maxY - self.r) if not self.intersects(existing): break else: self.x = x self.y = y

Žrebanje zapremo v neskončno zanko (while True). Prekinemo jo, čim ugotovimo, da smo izžrebali takšne koordinate, da žoga ne seka nobene druge (not self.intersects(existing)).

Za oceno 9

Strah in trepet domače naloge je bilo računanje skalarnega produkta, množenje vektorjev s skalarjem in odštevanje vektorjev.

Nauk 1: uporabljaj kratka, a jasna imena spremenljivk. Naredi dovolj spremenljivk, da se boš znašel in ne boš imel klobasastih izračunov. A niti ene več kot toliko.

def deflect_from(self, ball): nx, ny = ball.get_x() - self.x, ball.get_y() - self.y absn = sqrt(nx**2 + ny**2) nx /= absn ny /= absn scp = self.vx * nx + self.vy * ny bcp = ball.vx * nx + ball.vy * ny self.vx = self.vx - nx * scp + nx * bcp self.vy = self.vy - ny * scp + ny * bcp ball.vx = ball.vx + nx * scp - nx * bcp ball.vy = ball.vy + ny * scp - ny * bcp

Nauk 2: Matematika je tvoja prijateljica. \(v' = v_p + u_n = v - v_n + u_n = v - n (n\circ v) + n (n\circ u) = v - n (n\circ v - n\circ u) = v - n ( n \circ (v - u))\). Se pravi,izračunamo \(d = n (n \circ (v - u))\), pa imamo \(v' = v - d\) in \(u' = u + d\). Tako dobimo precej preprostejšo metodo.

def deflect_from(self, ball): nx, ny = ball.get_x() - self.x, ball.get_y() - self.y absn = sqrt(nx**2 + ny**2) nx /= absn ny /= absn scp = (self.vx - ball.vx) * nx + (self.vy - ball.vy) * ny dx, dy = nx * scp, ny * scp self.vx -= dx self.vy -= dy ball.vx += dx ball.vy += dy

Za oceno 10

def setup(n): krogle = [] for i in range(n): krogle.append(Ball(existing=krogle)) return krogle def igra(): krogle = setup(10) while True: for i, k in enumerate(krogle): k.move() for j in krogle[:i]: if k.touches(j): k.deflect_from(j) k.walls() risar.cakaj(0.01) igra()

Funkcija setup samo pametno dodaja krogle. V igri pa popazimo, da vsako kroglo od vsake odbijemo samo enkrat: poglejte, kako je narejena notranja zanka for.

Nekateri ste postavljali risar.cakaj znotraj zanke for. To ni tako dobra ideja, saj precej upočasni igro.

Last modified: Thursday, 25 March 2021, 9:35 PM