├── .github
├── TermiSand.png
└── example.png
├── LICENSE
├── README.md
├── TermiSand
├── TermiSand.exe
├── go.mod
├── go.sum
└── main.go
/.github/TermiSand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BobdaProgrammer/TermiSand/f6777a2ba954a3a68cf13e163dde9099c36a3d43/.github/TermiSand.png
--------------------------------------------------------------------------------
/.github/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BobdaProgrammer/TermiSand/f6777a2ba954a3a68cf13e163dde9099c36a3d43/.github/example.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 BobdaProgrammer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

TermiSand
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## Description
22 | TermiSand is a falling sand simulator, but in your terminal!
23 |
24 | ## Screenshot
25 |
26 |
27 | ## Usage
28 | Run the "TermiSand" executable
29 |
30 | and then move your cursor around and enjoy the show!
31 | to clear the screen press ctrl+r and watch as the sand falls off the screen ready for you to start filling it up again!
32 |
33 | ## Built with
34 | 
35 | 
36 |
--------------------------------------------------------------------------------
/TermiSand:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BobdaProgrammer/TermiSand/f6777a2ba954a3a68cf13e163dde9099c36a3d43/TermiSand
--------------------------------------------------------------------------------
/TermiSand.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BobdaProgrammer/TermiSand/f6777a2ba954a3a68cf13e163dde9099c36a3d43/TermiSand.exe
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/BobdaProgrammer/TermiSand
2 |
3 | go 1.21.6
4 |
5 | require github.com/gdamore/tcell/v2 v2.7.4
6 |
7 | require (
8 | github.com/gdamore/encoding v1.0.0 // indirect
9 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
10 | github.com/mattn/go-runewidth v0.0.15 // indirect
11 | github.com/rivo/uniseg v0.4.7 // indirect
12 | golang.org/x/sys v0.17.0 // indirect
13 | golang.org/x/term v0.17.0 // indirect
14 | golang.org/x/text v0.14.0 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
2 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
3 | github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
4 | github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
5 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
6 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
7 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
8 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
10 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
11 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
12 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
13 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
15 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
16 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
17 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
18 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
19 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
20 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
21 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
22 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
23 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
32 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
33 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
34 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
35 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
36 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
37 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
39 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
40 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
41 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
42 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
43 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
46 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
47 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
48 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "math"
6 | "math/rand"
7 | "os"
8 | "time"
9 |
10 | "github.com/gdamore/tcell/v2"
11 | )
12 |
13 | var (
14 | screenWidth, screenHeight int
15 | grid [][]int
16 | styleGrid [][]int
17 | lastMouseX, lastMouseY int
18 | mouseMoved bool
19 | hasFloor bool = true
20 | )
21 |
22 | func HSVtoRGB(hue int) (int32, int32, int32) {
23 | saturation := 1.0
24 | value := 1.0
25 |
26 | c := value * saturation
27 | x := c * (1 - math.Abs(math.Mod(float64(hue)/60.0, 2)-1))
28 | m := value - c
29 |
30 | var r, g, b float64
31 |
32 | switch {
33 | case hue >= 0 && hue < 60:
34 | r, g, b = c, x, 0
35 | case hue >= 60 && hue < 120:
36 | r, g, b = x, c, 0
37 | case hue >= 120 && hue < 180:
38 | r, g, b = 0, c, x
39 | case hue >= 180 && hue < 240:
40 | r, g, b = 0, x, c
41 | case hue >= 240 && hue < 300:
42 | r, g, b = x, 0, c
43 | case hue >= 300 && hue < 360:
44 | r, g, b = c, 0, x
45 | }
46 |
47 | r = (r + m) * 255
48 | g = (g + m) * 255
49 | b = (b + m) * 255
50 |
51 | return int32(r), int32(g), int32(b)
52 | }
53 |
54 | func BottomRowClear() bool {
55 | for x:=range grid[len(grid)-1]{
56 | if grid[len(grid)-1][x] != 0{
57 | return false
58 | }
59 | }
60 | return true
61 | }
62 |
63 | func resetGrid(s tcell.Screen){
64 | grid = make([][]int, screenHeight)
65 | styleGrid = make([][]int, screenHeight)
66 |
67 | for i := 0; i < screenHeight; i++ {
68 | grid[i] = make([]int, screenWidth)
69 | styleGrid[i] = make([]int, screenWidth)
70 | for x := 0; x < screenWidth; x++{
71 | s.SetContent(x, i, ' ', nil, tcell.StyleDefault)
72 | }
73 | }
74 | }
75 |
76 | func render(s tcell.Screen) {
77 |
78 | if !hasFloor{
79 | isclear:=true
80 | for x := range grid[len(grid)-1]{
81 | if grid[len(grid)-1][x] != 0{
82 | isclear=false;
83 | }
84 | grid[len(grid)-1][x]=0
85 | s.SetContent(x, screenHeight-1, ' ', nil, tcell.StyleDefault)
86 | }
87 | if isclear{
88 | hasFloor=true
89 | resetGrid(s)
90 | return
91 | }
92 | }
93 | for y := screenHeight - 2; y >= 0; y-- {
94 | for x := 0; x < screenWidth; x++ {
95 | blockstyle := tcell.StyleDefault.Background(tcell.NewRGBColor(HSVtoRGB(grid[y][x])))
96 | if grid[y][x] > 0 {
97 | if grid[y+1][x] == 0 {
98 | grid[y+1][x] = grid[y][x]
99 | grid[y][x] = 0
100 | s.SetContent(x, y+1, ' ', nil, blockstyle)
101 | styleGrid[y+1][x] = 1
102 | } else if x > 0 && grid[y+1][x-1] == 0 {
103 | grid[y+1][x-1] = grid[y][x]
104 | grid[y][x] = 0
105 | s.SetContent(x-1, y+1, ' ', nil, blockstyle)
106 | styleGrid[y+1][x-1] = 1
107 | } else if x < screenWidth-1 && grid[y+1][x+1] == 0 {
108 | grid[y+1][x+1] = grid[y][x]
109 | grid[y][x] = 0
110 | s.SetContent(x+1, y+1, ' ', nil, blockstyle)
111 | styleGrid[y+1][x+1] = 1
112 | }
113 | } else {
114 | style := tcell.StyleDefault
115 | if y != 0 && grid[y-1][x] != 0 {
116 | style = tcell.StyleDefault.Background(tcell.NewRGBColor(HSVtoRGB(grid[y-1][x])))
117 | grid[y][x] = grid[y-1][x]
118 | grid[y-1][x] = 0
119 | styleGrid[y][x] = 1
120 | } else {
121 | styleGrid[y][x] = 0
122 | }
123 | s.SetContent(x, y, ' ', nil, style)
124 | }
125 | if grid[y][x] != 0 && styleGrid[y][x] == 0 {
126 | s.SetContent(x, y, ' ', nil, tcell.StyleDefault.Background(tcell.NewRGBColor(HSVtoRGB(grid[y][x]))))
127 | }
128 | }
129 | }
130 | }
131 |
132 | func main() {
133 | s, err := tcell.NewScreen()
134 | if err != nil {
135 | log.Fatalf("TermISand couldn't initialise the screen :(")
136 | }
137 | defer s.Fini()
138 |
139 | if err := s.Init(); err != nil {
140 | log.Fatalf("TermISand couldn't initialise the screen :(")
141 | }
142 |
143 | s.EnableFocus()
144 | s.EnableMouse()
145 | screenWidth, screenHeight = s.Size()
146 | resetGrid(s)
147 |
148 | s.Clear()
149 | colorNum := 0
150 |
151 | eventCh := make(chan tcell.Event, 1)
152 |
153 | go func() {
154 | for {
155 | ev := s.PollEvent()
156 | if ev != nil {
157 | eventCh <- ev
158 | }
159 | }
160 | }()
161 |
162 | ticker := time.NewTicker(20 * time.Millisecond)
163 | defer ticker.Stop()
164 |
165 | for {
166 | select {
167 | case ev := <-eventCh:
168 | switch ev := ev.(type) {
169 | case *tcell.EventKey:
170 | switch ev.Key() {
171 | case tcell.KeyCtrlQ, tcell.KeyCtrlC:
172 | s.Fini()
173 | os.Exit(0)
174 | case tcell.KeyCtrlR:
175 | hasFloor = false
176 | }
177 | case *tcell.EventResize:
178 | screenWidth, screenHeight = s.Size()
179 | resetGrid(s)
180 | s.Sync()
181 | case *tcell.EventMouse:
182 | lastMouseX, lastMouseY = ev.Position()
183 | mouseMoved = true
184 | }
185 | case <-ticker.C:
186 | // Add sand at the last known mouse position if the mouse has moved
187 | if hasFloor && mouseMoved && lastMouseY < screenHeight && lastMouseX < screenWidth && grid[lastMouseY][lastMouseX] == 0 {
188 | grid[lastMouseY][lastMouseX] = colorNum
189 | rand1 := rand.Intn(4)
190 | rand2 := rand.Intn(4)
191 |
192 | if (rand1 == 0 || rand2 == 0) && lastMouseY-1 >= 0 && grid[lastMouseY-1][lastMouseX] == 0 {
193 | grid[lastMouseY-1][lastMouseX] = colorNum
194 | }
195 | if (rand1 == 1 || rand2 == 1) && lastMouseY+1 < screenHeight && grid[lastMouseY+1][lastMouseX] == 0 {
196 | grid[lastMouseY+1][lastMouseX] = colorNum
197 | }
198 | if (rand1 == 2 || rand2 == 2) && lastMouseX-1 >= 0 && grid[lastMouseY][lastMouseX-1] == 0 {
199 | grid[lastMouseY][lastMouseX-1] = colorNum
200 | }
201 | if (rand1 == 3 || rand2 == 3) && lastMouseX+1 < screenWidth && grid[lastMouseY][lastMouseX+1] == 0 {
202 | grid[lastMouseY][lastMouseX+1] = colorNum
203 | }
204 | colorNum++
205 | if colorNum == 360 {
206 | colorNum = 1
207 | }
208 | }
209 |
210 | render(s)
211 | s.Show()
212 |
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------