-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
437 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,14 @@ | ||
# cascade | ||
GameNGen Applied to falling sands. | ||
|
||
Falling Sand implementation taken from https://github.com/Antiochian/Falling-Sand/tree/master | ||
|
||
## Rough Plan: | ||
|
||
- [ ] Implement falling sands such that: | ||
- [ ] It can be played and the physics behaviour can be verified | ||
- [ ] It can be run in 'simulation' mode whereby an arbitrary number of game instances can be computer controlled at once and have their frames recorded | ||
- [ ] The backend can be swapped for a model that sends rending instructions to pygame | ||
- Query: Is pygame the best way to do this? | ||
- [ ] Construct a dataset | ||
- [ ] Construct a model |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
# from FallingSand import Particle | ||
import random | ||
import pygame | ||
|
||
scale = 2 | ||
aircolor = (0, 0, 0) | ||
allelements = {} | ||
changed = {} | ||
(Nx, Ny) = (400, 450) | ||
FPS = 20 | ||
|
||
yellow = (181, 137, 0) | ||
beige = (238, 232, 213) | ||
darkbeige = (171, 152, 92) | ||
orange = (203, 75, 22) # orange | ||
blue = (38, 139, 210) # blue | ||
red = (220, 50, 47) | ||
green = (133, 153, 0) | ||
grey = (88, 110, 117) | ||
magenta = (211, 54, 130) | ||
|
||
|
||
class Particle: | ||
def __init__(self, x, y, allelements, SURFACE): | ||
# allelements is a REFERENCE to a dictionary containing all element instances | ||
self.x = x | ||
self.y = y | ||
self.allelements = allelements | ||
self.SURFACE = SURFACE | ||
|
||
def checkkill(self, x, y): # checks to see if particle can be deleted | ||
if not 0 <= self.x <= Nx: | ||
self.draw(x, y, aircolor) # wipe pixel | ||
del self.allelements[(x, y)] | ||
return True | ||
elif not 0 <= self.y <= 300: | ||
self.draw(x, y, aircolor) | ||
del self.allelements[(x, y)] | ||
return True | ||
return False | ||
|
||
def checktarget(self, x, y): | ||
if ( | ||
self.allelements.get((x, y)) == None | ||
): # return whatever object is at target location, or False if not | ||
return True # if space is EMPTY return TRUE | ||
else: # if space is occupied, return FALSE (used to return occupier but i nixed that functionality) | ||
# return self.allelements.get( (x,y) ) | ||
return False | ||
|
||
def targetcolor( | ||
self, x, y | ||
): # very similar to the above, but instead of returning boolean, returns occupier object | ||
if ( | ||
self.allelements.get((x, y)) == None | ||
): # return whatever object is at target location, or False if not | ||
return self.allelements.get( | ||
(None, None) | ||
).color # if space is empty return NULLELEMENT | ||
else: | ||
return self.allelements.get((x, y)).color | ||
|
||
def draw(self, x, y, color): | ||
self.SURFACE.fill(color, pygame.Rect(x * scale, y * scale, scale, scale)) | ||
return | ||
|
||
def goto(self, newx, newy, overwritechance=0.0): | ||
global changed | ||
# SAND/WATER interaction - sand changes color and overwrites water | ||
if (self.color == beige or self.color == darkbeige) and self.targetcolor( | ||
newx, newy | ||
) == blue: | ||
self.color = darkbeige # CHANGE SAND COLOR TO WETSAND COLOR | ||
self.draw(newx, newy, self.color) | ||
overwritechance = 1 # set overwrite | ||
# WATER/SAND interaction - sand changes color but is not overwritten by water | ||
if self.color == blue and self.targetcolor(newx, newy) == beige: | ||
self.allelements[(newx, newy)].color = darkbeige | ||
self.draw(newx, newy, darkbeige) | ||
# WETSAND/DRYSAND interaction (wetness should spread slowly through sand) | ||
if ( | ||
self.color == darkbeige | ||
and self.targetcolor(newx, newy) == beige | ||
and random.random() < 0.08 | ||
): | ||
self.allelements[(newx, newy)].color = darkbeige | ||
self.draw(newx, newy, darkbeige) | ||
# LIQUID/LIQUID interaction | ||
|
||
# DEFAULT behaviour | ||
if ( | ||
self.checktarget(newx, newy) | ||
) or random.random() < overwritechance: # go ahead with move IF space is free | ||
(oldx, oldy) = (self.x, self.y) | ||
del self.allelements[ | ||
(oldx, oldy) | ||
] # delete current location from instance dictionary | ||
# self.SURFACE.fill(aircolor, pygame.Rect(oldx*self.scale, oldy*self.scale, self.scale, self.scale)) | ||
self.draw(oldx, oldy, aircolor) # delete old pixel | ||
(self.x, self.y) = (newx, newy) | ||
self.allelements[(newx, newy)] = self | ||
self.draw(newx, newy, self.color) | ||
# self.SURFACE.fill(self.color, pygame.Rect(newx*self.scale, newy*self.scale, self.scale, self.scale)) | ||
# mark locations as changed | ||
changed[(oldx, oldy)] = True | ||
changed[(newx, newy)] = True | ||
return True | ||
return False # otherwise return "failed" boolean | ||
|
||
|
||
class Metal(Particle): # metal just sits there and doesnt move | ||
def __init__(self, x, y, allelements, SURFACE): | ||
self.type = "solid" | ||
self.color = grey | ||
Particle.__init__(self, x, y, allelements, SURFACE) | ||
self.draw(self.x, self.y, self.color) | ||
|
||
def update(self): | ||
pass | ||
|
||
|
||
class Water(Particle): # water should flow and fall | ||
def __init__(self, x, y, allelements, SURFACE): | ||
self.type = "liquid" | ||
self.color = blue | ||
Particle.__init__(self, x, y, allelements, SURFACE) | ||
self.draw(self.x, self.y, self.color) | ||
|
||
def debug(self): # just to check if something exists | ||
print("Hello world, I am water!") | ||
print("My color is: ", self.color) | ||
|
||
def update(self): | ||
""" | ||
Water behaviour is like so: water is allowed to make 2-3 "actions" per turn | ||
it first tries to fall downward, with a chance to move left or right as it does so | ||
if it cant fall down it is then almost guaranteed to flow left or right | ||
if it hits a wall it will "reflect" off and move in the other direction | ||
""" | ||
if self.checkkill(self.x, self.y): | ||
return | ||
updates = 0 # start with zero actions | ||
flowdirection = ( | ||
random.randint(0, 1) * 2 - 1 | ||
) # returns +-1, decides if particle moves left or right | ||
if random.random() > 0.9: # small chance to not flow at all | ||
flowdirection = 0 # i.e: dont flow | ||
while updates < 2: | ||
if self.goto(self.x, self.y + 1): | ||
updates += 1 # log one cycle as complete | ||
if self.goto(self.x, self.y + 1): | ||
updates += 1 # log one cycle as complete | ||
if self.goto( | ||
self.x + flowdirection, self.y | ||
): # if space is available to go sideways | ||
pass | ||
elif self.goto( | ||
self.x - flowdirection, self.y | ||
): # if one side is blocked, "reflect" off other way | ||
flowdirection *= -1 | ||
updates += 0.67 | ||
|
||
|
||
class Acid(Particle): # like water, can eat through metal | ||
def __init__(self, x, y, allelements, SURFACE): | ||
self.type = "liquid" | ||
self.color = green | ||
Particle.__init__(self, x, y, allelements, SURFACE) | ||
self.draw(self.x, self.y, self.color) | ||
|
||
def debug(self): # just to check if something exists | ||
print("Hello world, I am acid!") | ||
print("My color is: ", self.color) | ||
|
||
def update(self): | ||
""" | ||
ACID behaves like water but has a certain chance to eat through containing | ||
materials, defined in the variable "acidchance" | ||
""" | ||
if self.checkkill(self.x, self.y): | ||
return | ||
acidchance = 0.01 | ||
updates = 0 # start with zero actions | ||
flowdirection = ( | ||
random.randint(0, 1) * 2 - 1 | ||
) # returns +-1, decides if particle moves left or right | ||
if random.random() > 0.9: # small chance to not flow at all | ||
flowdirection = 0 # i.e: dont flow | ||
while updates < 2: | ||
if self.goto(self.x, self.y + 1, acidchance): | ||
updates += 1 # log one cycle as complete | ||
if self.goto(self.x, self.y + 1, acidchance): | ||
updates += 1 # log one cycle as complete | ||
if self.goto( | ||
self.x + flowdirection, self.y, acidchance | ||
): # if space is available to go sideways | ||
pass | ||
elif self.goto( | ||
self.x - flowdirection, self.y, acidchance | ||
): # if one side is blocked, "reflect" off other way | ||
pass | ||
updates += 1 | ||
|
||
|
||
class Sand( | ||
Particle | ||
): # sand behaves like a very viscous liquid, BUT is CLASSED as a solid | ||
def __init__(self, x, y, allelements, SURFACE): | ||
self.type = "solid" | ||
self.color = beige | ||
self.flowchance = ( | ||
0.05 # chance to behave as liquid per tick (CAN CHANGE IF WET) | ||
) | ||
Particle.__init__(self, x, y, allelements, SURFACE) | ||
self.draw(self.x, self.y, self.color) | ||
|
||
def debug(self): # just to check if something exists | ||
print("Hello world, I am sand!") | ||
print("My color is: ", self.color) | ||
|
||
def update(self): | ||
""" | ||
Sand is like water but it hardly ever flows sideways, and if it gets wet | ||
then it solidifies and becomes immovable. Wet sand slowly "infects" nearby dry sand | ||
(This behaviour is codified inside the goto function) | ||
""" | ||
if self.checkkill(self.x, self.y): | ||
return | ||
updates = 0 # start with zero actions | ||
|
||
if self.color == beige: | ||
flowchance = 0.05 # 5% chanc eto flow per tick if dry | ||
elif self.color == darkbeige: | ||
flowchance = 0 # never flow if wet | ||
|
||
flowdirection = ( | ||
random.randint(0, 1) * 2 - 1 | ||
) # returns +-1, decides if particle moves left or right | ||
if random.random() > flowchance: # LARGE chance to not flow at all for sand | ||
flowdirection = 0 # i.e: dont flow | ||
while updates < 2: | ||
if self.goto( | ||
self.x, self.y + 2 | ||
): # if space is available to fall down 2 spaces | ||
updates += 2 | ||
elif self.goto(self.x, self.y + 1): | ||
updates += 1 # log one cycle as complete | ||
if self.goto( | ||
self.x + flowdirection, self.y | ||
): # if space is available to go sideways | ||
pass | ||
# elif self.goto(self.x - flowdirection, self.y): #if one side is blocked, "reflect" off other way | ||
# pass | ||
updates += 2 | ||
|
||
|
||
class NullElement(Particle): # this placeholder sits at (None,None) and does NOTHING | ||
def __init__(self, allelements, SURFACE): | ||
self.color = None | ||
self.x = None | ||
self.y = None | ||
Particle.__init__(self, self.x, self.y, allelements, SURFACE) | ||
|
||
def update(self): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import pygame | ||
import sys | ||
import numpy as np | ||
from FallingSand_Elements import * | ||
|
||
|
||
def globalchecktarget(x, y): | ||
if ( | ||
allelements.get((x, y)) == None | ||
): # return whatever object is at target location, or False if not | ||
return True # if space is EMPTY return TRUE | ||
else: # if space is occupied, return FALSE (used to return occupier but i nixed that functionality) | ||
return False # return self.allelements.get( (x,y) ) | ||
|
||
|
||
def pendraw(elementtype, x, y, pensize): # DOES NOT ACCEPT DECIMAL POSITIONS | ||
# this function places a suitable number of elements in a circle at the position specified | ||
if pensize == 0 and globalchecktarget(x + xdisp, y + ydisp): | ||
allelements[(x, y)] = elementtype(x, y, allelements, SURFACE) # place 1 pixel | ||
else: | ||
for xdisp in range(-pensize, pensize): # penzize is the radius | ||
for ydisp in range(-pensize, pensize): | ||
if globalchecktarget(x + xdisp, y + ydisp): | ||
allelements[(x + xdisp, y + ydisp)] = elementtype( | ||
x + xdisp, y + ydisp, allelements, SURFACE | ||
) | ||
return | ||
|
||
|
||
pygame.init() | ||
window = pygame.display.set_mode((Nx, Ny)) | ||
pygame.display.set_caption("Antioch's Falling Sand") | ||
|
||
SURFACE = window.copy() # SURFACE is where all the magic will happen | ||
|
||
|
||
###DEBUG INITIALISATION### | ||
# | ||
##for x in range(50,100): | ||
## for y in range(24,26): | ||
## allelements[(x,y)] = Water(x,y,allelements,SURFACE) | ||
# #SURFACE.fill((255,255,255), pygame.Rect(x*scale, y*scale, scale, scale)) | ||
for x in range(70, 150): # build testing bucket | ||
y = 150 | ||
allelements[(x, y)] = Metal(x, y, allelements, SURFACE) | ||
allelements[(x, y + 1)] = Metal(x, y + 1, allelements, SURFACE) | ||
for y in range(138, 150): | ||
if y in range(145, 150): | ||
allelements[(70, y)] = Metal(70, y, allelements, SURFACE) | ||
allelements[(80, y)] = Metal(80, y, allelements, SURFACE) | ||
allelements[(149, y)] = Metal(149, y, allelements, SURFACE) | ||
|
||
########################## | ||
window.blit(SURFACE, (0, 0)) | ||
clock = pygame.time.Clock() | ||
pygame.display.update() | ||
ActiveElement = Metal # default | ||
pensize = 1 | ||
|
||
# INITIALISE NULLELEMENT | ||
allelements[(None, None)] = NullElement(allelements, SURFACE) | ||
while True: | ||
changed = {} | ||
clock.tick(FPS) | ||
|
||
for event in pygame.event.get(): # detect events | ||
if event.type == pygame.QUIT: # detect attempted exit | ||
pygame.quit() | ||
sys.exit() | ||
if pygame.mouse.get_pressed()[0]: | ||
pendraw( | ||
ActiveElement, | ||
int(pygame.mouse.get_pos()[0] / scale), | ||
int(pygame.mouse.get_pos()[1] / scale), | ||
pensize, | ||
) | ||
pressed_keys = pygame.key.get_pressed() | ||
if pressed_keys[49]: | ||
pensize = 1 | ||
ActiveElement = Metal | ||
if pressed_keys[50]: | ||
pensize = 2 | ||
ActiveElement = Water | ||
if pressed_keys[51]: | ||
pensize = 2 | ||
ActiveElement = Sand | ||
if pressed_keys[52]: | ||
pensize = 2 | ||
ActiveElement = Acid | ||
|
||
# update screen | ||
window.blit(SURFACE, (0, 0)) | ||
for element in list(allelements.keys()): | ||
try: | ||
allelements[element].update() | ||
except KeyError: | ||
pass | ||
|
||
# if element not in changed: | ||
# allelements[element].update() | ||
pygame.display.update() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[project] | ||
name = "cascade" | ||
version = "0.1.0" | ||
description = "Add your description here" | ||
readme = "README.md" | ||
requires-python = ">=3.10" | ||
dependencies = [ | ||
"pygame>=2.6.1", | ||
] |
Oops, something went wrong.