Front Office Football Central  

Go Back   Front Office Football Central > Main Forums > Dynasty Reports
Register FAQ Members List Calendar Mark Forums Read Statistics

Reply
 
Thread Tools
Old 10-26-2021, 06:27 PM   #1
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Let's Learn Some Programming Languages (Go, Scala, Rust)

I suppose I should have known better with how nerdy this forum is, but I didn't realize we had so many programmers on the forum until Shane's post. For the last few months, I've been making a concerted effort to expand my repertoire of programming languages, frameworks, etc. that I am familiar with. As it is right now, I'm not that well rounded.

For the last 10+ years, I've mostly been focused on C#, done some things in WPF, but mostly used Unity. Some years ago, I also picked up Python, and I've mainly used that for some web scarping and machine learning, but nothing too advanced.

So I like to think I'm a pretty decent C# coder, able to do quite a lot in Unity, do a little in WPF. Last year, with all of the extra time I had with being off work for part of the year, I threw myself into levelling up my C# and that, at least I think, paid off. I feel like I'm a much better C# programmer than I was 18 months ago.

But now I'm looking to expand my skillset while I spend most of my time getting back to my Unity/C# projects. So most days I'll devote an hour or so towards some new languages, some new frameworks, some new libraries, etc. Obviously I won't be getting all of these up to Level 80 (to throw out an old WoW reference. Haven't played that game in forever. Don't even know if 80 is still the max level), but I'm hoping a few really grab me and I run with them. The two areas that interest me the most are game programming and machine learning, so considering what little I already know, Scala (and using it with Apache Spark) might be the early favorite.

Anyway, enough rambling. The first language up is Go and today I finished my first game, it's a very simple number guessing game. Let's take a look at it.


Last edited by sabotai : 10-26-2021 at 06:50 PM.
sabotai is offline   Reply With Quote
Old 10-26-2021, 06:30 PM   #2
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
I'm going to set up a public guthub for these projects soon, but here's the Go code (don't judge if you're a Gopher, first project, haven't completely cleaned up the code). I'll explain some of the code in the next post.

Code:
package main import ( "GuessNumber/gojaygo" "bufio" "fmt" "os" "strconv" "strings" ) func main() { randomGenerator := gojaygo.NewRandomGenerator() number := randomGenerator.NextInt(1, 20) reader := bufio.NewReader(os.Stdin) finalGuess := 0 guessesTaken := 0 for ; guessesTaken < 5; { fmt.Println("Take a Guess.") inGuess, _ := reader.ReadString('\n') inGuess = strings.TrimRight(inGuess, "\r\n") guess, err := strconv.Atoi(inGuess) if err != nil { fmt.Println("Something went wrong, please try again.") continue } fmt.Printf("You guess: %v \n", guess) guessesTaken = guessesTaken + 1 if guess < number { fmt.Println("Your guess is too low.") } else if guess > number { fmt.Println("Your guess is too high.") } else { finalGuess = guess break } } if finalGuess == number { fmt.Printf("You win! You guessed the correct number in %v guesses.\n", guessesTaken) } else { fmt.Printf("Sorry, the correct number was %v.\n", number) } }

Last edited by sabotai : 10-26-2021 at 06:30 PM.
sabotai is offline   Reply With Quote
Old 10-26-2021, 06:50 PM   #3
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
First things first, even if you don't know Go, the "GuessNumber/gojaygo" might stick out to you.

As Drake said in Shane's thread about building up your own personal library...that's what that is. My personal package for Go code is called GoJayGo, and one of the first things I build in a new language (apparently) is a random number generator. Why make my own instead of just directly using rand from the standard library? 3 Reason:

1) I don't know what I'll find in Go, but in Unity using C#, both System and UnityEngine have a class called Random. And as you might suspect, using both System and UnityEngine in your Unity scripts is quite common, so every time you type Random. your IDE screams at you. So, I made a static class in my personal library to generate random numbers. Off the top of my head, I don't even remember which Random I used (though probably System)

2) I know from experience that there are some common ways of generating random numbers that I use over and over again and it's better to create methods for those situations. For example, to get a number based on rolling a die, you would type Random.NextInt(0, 6) + 1 or Random.NextInt(1, 7). IMO, it's easier to just type Random.DiceRoll(6). And of course, it's very common to want multiple dice, so Random.DiceRoll(6, 3) (6 sided die, 3 times).

(Of course, if you're going to visualize or otherwise tell the player how each die came out, you'll have to roll each die by itself).

Also, I...loathe...max exclusive functions. I'll spare you the rant I had in my head about it, but I think it's horribly unintuitive to have min inclusive, max exclusive parameters and just because most coders have gotten used to it does not mean it isn't fucking terrible, because it is. I make my random ints max inclusive

3) At some point, I'm going to have multiple threads getting random numbers, and with it the chance that multiple threads will try to grab a random number at the same exact time which could lead to some disastrous outcomes. The standard random number generators in most language are not thread safe. So to do that I'll have to do something like

Lock
Get Random Number
Unlock

every time I get a random number. 3 lines doing the job of 1. It'd be better if I just had my own class that generated random numbers in a thread safe way so I never have to worry about that elsewhere in the code.
sabotai is offline   Reply With Quote
Old 10-26-2021, 06:56 PM   #4
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Here's my first go at my own Random struct.

Code:
package gojaygo import ( "math/rand" "time" ) type Random struct { rand *rand.Rand } func NewRandomGenerator() *Random { s1 := rand.NewSource(time.Now().UnixNano()) r1 := rand.New(s1) ra1 := Random{ rand: r1, } return &ra1 } // NextInt max inclusive func (r *Random) NextInt(min int, max int) int { return r.rand.Intn(max+1) + min } func (r *Random) NextFloat32(min float32, max float32) float32 { return r.rand.Float32() * (max - min) + min } func (r *Random) NextFloat64(min float64, max float64) float64 { return r.rand.Float64() * (max - min) + min } func (r *Random) DiceRoll(sides int) int { return r.NextInt(1, sides) } func (r *Random) DiceRollN(sides int, times int) int { result := 0 for i := 0; i < times; i++ { result = result + r.DiceRoll(sides) } return result } func (r *Random) CoinFlip() int { return r.NextInt(0, 1) }

....just in case we have any Go programmers on the forum that can rip this code to shreds I guess. This version is not thread-safe. I'll work on that in the future.

Last edited by sabotai : 10-27-2021 at 07:22 AM.
sabotai is offline   Reply With Quote
Old 10-26-2021, 07:13 PM   #5
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
For the programmers on the forum that don't know Go, I'll try to explain some of the idiosyncrasies of the language (and in the process hopefully solidify these things in my own head, although it's possible....likely...that something I say here will be wrong).

You'll notice the code seems to use both = and := to assign values. Go is a strongly typed language, but the complier can also 'guess' as to what you type a variable is when being created. Using the := tells the compiler to do that.

number := randomGenerator.NextInt(1, 20)

creates the variable "number" and assigns it the result from the random number generator. And since NextInt is returning an Int, the compiler knows what type to give it.

The long way to do it would be

var number int = randomGenerator.NextInt(1, 20)


Functions in Go can also return multiple variables without them needing to be a tuple.

guess, err := strconv.Atoi(inGuess)

returns two values, an integer (this is converting a string to an integer), and an Error if it fails.


The for loop also looks a bit funky. In Go, all loops are for loops.

for ; guessesTaken < 5; {

is how you start a loop that behaves like a while loop in Go. The variable is already declared, so no need to create it in the first spot, and the incrementing happens in the block instead of after each iteration so no need to put that in the third spot.

Last edited by sabotai : 10-26-2021 at 07:22 PM.
sabotai is offline   Reply With Quote
Old 10-26-2021, 07:22 PM   #6
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
And a quick note about my random number class.

There aren't the typical "classes" in Go. There are Structs and there are Functions, but can make a function a method of a Struct with the following syntax

func (r *Random) NextInt(min int, max int) int {

the (r *Random) part makes the function work off of the Random struct (the * makes it a pointer so it works off the struct itself. Without the asterix, it will work off a copy).

That also means there aren't your traditional constructors. So you can't just go

randomGenerator := new gojaygo.Random();

and stick some initialization code in Random's constructor.

So instead there's the NewRandomGenerator function that creates a new Random in memory and returns the pointer to that.



Anyway, if you have any questions I'll try to answer them. And if you're a Gopher and have any questions about why I didn't do this or that, I JUST STARTED! But also please tell me where my codes sucks and why.
sabotai is offline   Reply With Quote
Old 10-27-2021, 02:37 PM   #7
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Some more info I meant to include in my first post. I should stop posting when I'm dead tired.


I started learning Go about a month ago. I started by browsing golang.org, starting with a tour of go: A Tour of Go

Over the last month, I watched a lot of videos on Pluralsight and some on YouTube. Then a few days ago, I started on the number guessing game.

I'll be going through these three books to learn the basics of Go

Invent Your Own Computer Games with Python
Impractical Python Projects
Automate the Boring Stuff with Python


These three books are loaded with simple projects in Python. The first book, Invent You Own Computer Games, has about 15-20 mostly text based games. I'll be recreating those in Go, though probably not every single one. When I don't know how to do something, I'll google it. The number guessing game was game #1 in that book.

So why use these? These types of books do not exist for Go. They don't exist for the vast majority of languages. The massive amount of information that's out there for learning Python is astounding to me. Really nothing like it in any other language. I wish these three books (and others) had counterparts in all of the languages I'm interested in. But instead Go has about a dozen or so that are the typical boring types of books. The same for Rust. The same for Scala.

Python gets to have all the fun.


After that, I'll grab a few more interesting books or start making my own more complex games and apps.

Eventually, I'd like to make a very simple version of at least 1 of the following:
2D or 3D graphics engine
2D or 3D physics engine
Machine Learning/Deep Learning library
Game AI library
sabotai is offline   Reply With Quote
Old 10-27-2021, 03:21 PM   #8
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Second game in the book was a joke telling program. Pass.

Next up, Dragon Realm. The world's most basic text adventure game. You stand in front of 2 caves, and you have to choose 1. One leads to treasure, the other to certain death.

The code is 85 lines long, so I won't paste it here. Instead I'll show show the output of the game after a few play throughs

You are in a land full of dragons. In front of you, you see two caves.
In one cave, the dragon is friendly and will share his treasure with you.
The other dragon is greedy and hungry, and will eat you on sight.
Which cave would you like to enter? (1 or 2)
1
You approach cave #1...
It is dark and spooky
A large dragon jumps out at you and...
Gobbles you down in one bite!

Do you want to play again? (Y or N)
Y
You are in a land full of dragons. In front of you, you see two caves.
In one cave, the dragon is friendly and will share his treasure with you.
The other dragon is greedy and hungry, and will eat you on sight.
Which cave would you like to enter? (1 or 2)
2
You approach cave #2...
It is dark and spooky
A large dragon jumps out at you and...
Gives you his treasure!

Do you want to play again? (Y or N)
Y
You are in a land full of dragons. In front of you, you see two caves.
In one cave, the dragon is friendly and will share his treasure with you.
The other dragon is greedy and hungry, and will eat you on sight.
Which cave would you like to enter? (1 or 2)
1
You approach cave #1...
It is dark and spooky
A large dragon jumps out at you and...
Gobbles you down in one bite!

Do you want to play again? (Y or N)


You can't tell by the output, but the cave is chosen randomly each time.

Also, there is a 2 second long wait after each line that ends with "..."

Code:
fmt.Printf("You approach cave #%v...\n", cave) time.Sleep(2 * time.Second) fmt.Println("It is dark and spooky") time.Sleep(2 * time.Second) fmt.Println("A large dragon jumps out at you and...") time.Sleep(2 * time.Second)
sabotai is offline   Reply With Quote
Old 10-27-2021, 08:48 PM   #9
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
For those who want to see the code for Dragon Ream: GitHub - JayFromNJ/DragonRealm
sabotai is offline   Reply With Quote
Old 10-28-2021, 03:57 PM   #10
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Next Up, Hangman

GitHub - JayFromNJ/Hangman

This was the first real challenge from the book, String manipulation is a lot easier in Python, and I just did a ton of googling to figure out what I could and could not do in Go.

The function that displays the board, specifically the part that creates the string so that the word looks like "___e_t" based on the correctly guessed letters is different from the Python book. I started and stopped a half dozen ways to do it until settling on what I did.

I also had to create my own Contains function for testing if an array of strings contained a certain value. In Go, you can use

strings.Contains

to see if a string contains a letter or string, but for arrays you have to do that work yourself. Python and C# have spoiled me.
sabotai is offline   Reply With Quote
Old 10-31-2021, 04:48 PM   #11
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
The next chapter is on extending Hangman and the one after is Tic-Tac-To. Meh. Moving on.

Chapter 11: The Bagels Deduction Game.

Bagels is a deduction game. The game creates a number out of 3 random, non-repeating digits. After each guess, the player is given three types of clue:

Bagels None of the three digits guessed is in the secret number.
Pico One of the digits is in the secret number, but the guess has the digit in the wrong place.
Fermi The guess has a correct digit in the correct place.

If the secret number is 456 and the player’s guess is 546, the clues would be “fermi pico pico.” The “fermi” is from the 6 and “pico pico” are from the 4 and 5.

Here is the output from the game from the book

Code:
I am thinking of a 3-digit number. Try to guess what it is. The clues I give are... When I say: That means: Bagels None of the digits is correct. Pico One digit is correct but in the wrong position. Fermi One digit is correct and in the right position. I have thought up a number. You have 10 guesses to get it. Guess #1: 123 Fermi Guess #2: 453 Pico Guess #3: 425 Fermi Guess #4: 326 Bagels Guess #5: 489 Bagels Guess #6: 075 Fermi Fermi Guess #7: 015 Fermi Pico Guess #8: 175 You got it!
sabotai is offline   Reply With Quote
Old 10-31-2021, 04:56 PM   #12
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
But first, my second go file for my gojaygo package

Code:
package gojaygo func StringArrayContains(strArray []string, toFind string) bool { for _, a := range strArray { if a == toFind { return true } } return false } func IntArrayContains(intArray []int, toFind int) bool { for _, a := range intArray { if a == toFind { return true } } return false } func Float32ArrayContain(floatArray []float32, toFind float32) bool { for _, a := range floatArray { if a == toFind { return true } } return false } func Float64ArrayContains(floatArray []float64, toFind float64) bool { for _, a := range floatArray { if a == toFind { return true } } return false }

Some quick and dirty functions to check if a simple array contains a certain value at least once.

Last edited by sabotai : 10-31-2021 at 04:56 PM.
sabotai is offline   Reply With Quote
Old 11-01-2021, 03:33 PM   #13
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Output from my Go Bagels game

Code:
PS C:\Users\Jason\go\src\Bagels> go run main.go I am thinking of a 3-digit number. Try to guess what it is. The clues I give are... Clue That means: Bagels None of the digits are correct Pico One digit is correct but in the wrong position Fermi One digit is correct and in the right position I have thought of a number. You have 10 guess to get it. Guess #1: 456 Fermi Guess #2: 356 Fermi Guess #3: 257 Fermi Fermi Guess #4: 258 Fermi Fermi Guess #5: 259 Fermi Fermi Guess #6: 250 Fermi Fermi Guess #7: 251 You got it! Do you want to play again? (Y or N) n PS C:\Users\Jason\go\src\Bagels>
sabotai is offline   Reply With Quote
Old 11-01-2021, 03:41 PM   #14
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
GitHub - JayFromNJ/Bagels

So this one doesn't even use either go file from my personal package. That's because the base rand class includes a Shuffle function for shuffling the contents of an array. That's handy. Here it is in action:

Code:
func getSecretNumber() string { numbers := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} rand.Seed(time.Now().UnixNano()) rand.Shuffle(len(numbers), func(i, j int) { numbers[i], numbers[j] = numbers[j], numbers[i] }) toReturn := "" for i := 0; i < NUM_DIGITS; i++ { toReturn = toReturn + numbers[i] } return toReturn }

The shuffle function is simple enough. Tell it how many items the array is and tell it what to swap.

And Go gives you a shorthand for parameters of the same type. Instead of having to type out (i int, j int), you can shorten to (i, j int)

Last edited by sabotai : 11-01-2021 at 03:42 PM.
sabotai is offline   Reply With Quote
Old 11-01-2021, 03:51 PM   #15
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
So I finally got tired of always having to trim the ends of the input strings and just made a class to handle it.

File #3 in gojaygo, input.go

Code:
package gojaygo import ( "bufio" "os" "strings" ) type CLInput struct { CLReader bufio.Reader } func GetNewCLReader() CLInput { return CLInput{ CLReader: *bufio.NewReader(os.Stdin), } } func (cli *CLInput) GetCLInput() (string, error) { toReturn, err := cli.CLReader.ReadString('\n') if err != nil { return "", err } toReturn = strings.TrimRight(toReturn, "\r\n") return toReturn, nil }
sabotai is offline   Reply With Quote
Old 11-02-2021, 09:02 PM   #16
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Ok, I'm bored now. This has gotten tedious. I skimmed through the other two books and the most interesting projects in them lean heavily on PyGame or matplotlib, stuff not available to Go. I'm just going to move on to a far more interesting project.

I've read the first few chapters of The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer by Jamis Buck.

The end project is a ray-tracer that renders 3D scenes to an image file. The book teaches through some pseudo-code but mainly by giving you the unit tests for each function/system and you build the ray-tracer piece by piece, hence "test-driven". You design the unit test before you design the function/system.

A few reasons I'm jumping to this far more advanced project:

1) It'll help me build a large math library for me to use in other projects that needs vectors, points, matrices, etc.
2) It'll give me a ton of practice using Go's unit testing
3) It's cool and interesting

So let's get started!

Last edited by sabotai : 11-02-2021 at 09:27 PM.
sabotai is offline   Reply With Quote
Old 11-02-2021, 09:26 PM   #17
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
The first thing I do is create a struct that holds 4 values to represent a point or a vector. The first 3 values are x, y and z. The 4th is w and for a point, it's equal to 0.0 and for a vector (direction) it's 1.0. EDIT: No it's not dummy! You switched them. Points are 1.0 and Vectors have a w of 0.0. I've edited the code, tests and the posts to reflect the fix.

So the very first unit tests will be to create a point and create a vector and make sure they were created correctly.

Here's the code for the Vector struct (both points and vectors will be called Vector. After years of using Unity I'm used to both position and velocity being Vectors). I thought of giving it a more generic name like FourDim or something, but I'm just used to Vector.
vector.go
Code:
package vector type Vector struct { x, y, z, w float32 } func NewVector(x, y, z float32) Vector { return Vector{ x: x, y: y, z: z, w: 0.0, } } func NewPoint(x, y, z float32) Vector { return Vector{ x: x, y: y, z: z, w: 1.0, } }

And here are the unit tests for the two functions

vector_test.go
Code:
package vector import "testing" func TestNewVector(t *testing.T) { v := NewVector(4.3, -4.2, 3.1) if v.x != 4.3 || v.y != -4.2 || v.z != 3.1 || v.w != 0.0 { t.Fail() } } func TestNewPoint(t *testing.T) { p := NewPoint(4.3, -4.2, 3.1) if p.x != 4.3 || p.y != -4.2 || p.z != 3.1 || p.w != 1.0 { t.Fail() } }

And finally, the output of running the tests

Code:
PS C:\Users\Jason\go\src\RayTracer\vector> go test -v === RUN TestNewVector --- PASS: TestNewVector (0.00s) === RUN TestNewPoint --- PASS: TestNewPoint (0.00s) PASS ok RayTracer/vector 0.066s PS C:\Users\Jason\go\src\RayTracer\vector>

Last edited by sabotai : 11-02-2021 at 09:50 PM.
sabotai is offline   Reply With Quote
Old 11-02-2021, 09:47 PM   #18
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Next up is adding two vectors together. I'm going to go the full nine yards with this, so keeping with the Test Driven Development the book is going for, I wrote out the next unity test first.

Code:
func TestAddVectors(t *testing.T) { v := NewVector(3.0, -2.0, 5.0) p := NewPoint(-2.0, 3.0, 1.0) res := v.Add(p) if res.x != 1.0 || res.y != 1.0 || res.z != 6.0 || res.w != 1.0 { t.Fail() } }

Vectors in Unity are value types. Every method that modifies the vector returns a new vector and that's how I'm used to them working so that's how I'll make my vectors.

Now I need to code the Add function

Code:
func (v *Vector) Add(add Vector) Vector { return Vector{ x: v.x + add.x, y: v.y + add.y, z: v.z + add.z, w: v.w + add.w, } }

And the result of the unit tests now

Code:
PS C:\Users\Jason\go\src\RayTracer\vector> go test -v === RUN TestNewVector --- PASS: TestNewVector (0.00s) === RUN TestNewPoint --- PASS: TestNewPoint (0.00s) === RUN TestAddVectors --- PASS: TestAddVectors (0.00s) PASS ok RayTracer/vector 0.064s
sabotai is offline   Reply With Quote
Old 11-02-2021, 09:48 PM   #19
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Oh crap, I made a mistake already! Points should have 1.0 as their w value and Vectors should be at 0.0. I switched them. Opps. Fixed that.
sabotai is offline   Reply With Quote
Old 11-02-2021, 09:52 PM   #20
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Also, the book points out that by adding two points, you get a w of 2.0, which is neither a vector nor a point. But it also doesn't say anything about fixing that or what to do in that situation. So....I guess I'll leave it for now.

I mean. here's what he says:

"And check out how that w coordinate cooperates. You add a point (w of 1) and a vector (w of 0), and the result has a w of 1—another point! Similarly, you could add two vectors (w of 0) and get a vector, because the w’s sum to 0. However, adding a point to a point doesn’t really make sense. Try it. You’ll see that you get a tuple with a w of 2, which is neither a vector nor a point!"

Last edited by sabotai : 11-02-2021 at 10:26 PM.
sabotai is offline   Reply With Quote
Old 11-02-2021, 10:14 PM   #21
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Next up Subtract. Unit Test is similar to the add.

And to give you an idea of how the book presents the design for the test, here's the fomat:

Code:
Scenario: Subtracting two points Given p1 ← point(3, 2, 1) And p2 ← point(5, 6, 7) Then p1 - p2 = vector(-2, -4, -6)

And he mentions some system called Cucumber. For anyone who's ever used that.

Anyway, the Subtract unit test

BTW, for anyone wondering, Go does not support operator overloading, so I can't just overload + and write res := vector1 + vector2.

Code:
func TestSubtractVectors(t *testing.T) { p1 := NewPoint(3.0, 2.0, 1.0) p2 := NewPoint(5.0, 6.0, 7.0) res := p1.Subtract(p2) if res.x != -2.0 || res.y != -4.0 || res.z != -6.0 || res.w != 0.0 { t.Fail() } }

And now the code...

Code:
func (v *Vector) Subtract(minus Vector) Vector { return Vector{ x: v.x - minus.x, y: v.y - minus.y, z: v.z - minus.z, w: v.w - minus.w, } }

And the unit tests now...

Code:
PS C:\Users\Jason\go\src\RayTracer\vector> go test -v === RUN TestNewVector --- PASS: TestNewVector (0.00s) === RUN TestNewPoint --- PASS: TestNewPoint (0.00s) === RUN TestAddVectors --- PASS: TestAddVectors (0.00s) === RUN TestSubtractVectors --- PASS: TestSubtractVectors (0.00s) PASS ok RayTracer/vector 0.065s

Of course, we get the reverse of the addition situation (and to author doesn't even mention it). Subtracting a point from a vector makes no sense, and results in a w of -1.0. I guess I'll just have to make sure I don't do that.

The reason I'm not jumping in front of this issue is because I've followed a ton of these types of books (and video series). If I do code a check for these and make sure they don't happen, I might screw up something in the future that the author simply hasn't divulged to me yet. Since the author hasn't told me to make sure it doesn't happen, I'll leave it and if it ever becomes a problem, then I'll deal with it then.
sabotai is offline   Reply With Quote
Old 11-02-2021, 10:20 PM   #22
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Subtraction actually had three unit tests. Point - Point, Point - Vector, Vector - Vector

Code:
Scenario: Subtracting two points Given p1 ← point(3, 2, 1) And p2 ← point(5, 6, 7) Then p1 - p2 = vector(-2, -4, -6) Scenario: Subtracting a vector from a point Given p ← point(3, 2, 1) And v ← vector(5, 6, 7) Then p - v = point(-2, -4, -6) Scenario: Subtracting two vectors Given v1 ← vector(3, 2, 1) And v2 ← vector(5, 6, 7) Then v1 - v2 = vector(-2, -4, -6)

So I'll update the test function (I shouldn't need to touch my Subtract function

Code:
func TestSubtractVectors(t *testing.T) { p1 := NewPoint(3.0, 2.0, 1.0) p2 := NewPoint(5.0, 6.0, 7.0) v1 := NewVector(3.0, 2.0, 1.0) v2 := NewVector(5.0, 6.0, 7.0) res1 := p1.Subtract(p2) if res1.x != -2.0 || res1.y != -4.0 || res1.z != -6.0 || res1.w != 0.0 { t.Fail() } res2 := p1.Subtract(v2) if res2.x != -2.0 || res2.y != -4.0 || res2.z != -6.0 || res2.w != 1.0 { t.Fail() } res3 := v1.Subtract(v2) if res3.x != -2.0 || res3.y != -4.0 || res3.z != -6.0 || res3.w != 0.0 { t.Fail() } }

And the unit tests all passed.
sabotai is offline   Reply With Quote
Old 11-03-2021, 03:14 PM   #23
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
I finished off Chapter 1. Here is the vector class as it is now.

Code:
package vector import "math" type Vector struct { x, y, z, w float64 } func (v *Vector) X() float64 { return v.x } func (v *Vector) Y() float64 { return v.y } func (v *Vector) Z() float64 { return v.z } func (v *Vector) W() float64 { return v.w } func NewVector(x, y, z float64) Vector { return Vector{ x: x, y: y, z: z, w: 0.0 } } func NewPoint(x, y, z float64) Vector { return Vector{ x: x, y: y, z: z, w: 1.0 } } func ZeroVector() Vector { return NewVector(0.0, 0.0, 0.0) } func ZeroPoint() Vector { return NewPoint(0.0, 0.0, 0.0) } func (v *Vector) Add(value Vector) Vector { return Vector{ x: v.x + value.x, y: v.y + value.y, z: v.z + value.z, w: v.w + value.w, } } func (v *Vector) Subtract(value Vector) Vector { return Vector{ x: v.x - value.x, y: v.y - value.y, z: v.z - value.z, w: v.w - value.w, } } func (v *Vector) Negate() Vector { return NewVector(-v.x, -v.y, -v.z) } func (v *Vector) Multiply(value float64) Vector { return Vector{ x: v.x * value, y: v.y * value, z: v.z * value, w: v.w * value, } } func (v *Vector) Divide(value float64) Vector { return Vector{ x: v.x / value, y: v.y / value, z: v.z / value, w: v.w / value, } } func (v *Vector) Magnitude() float64 { val := (v.x * v.x) + (v.y * v.y) + (v.z * v.z) return math.Sqrt(val) } func (v *Vector) Normalize() Vector { return v.Divide(v.Magnitude()) } func (v *Vector) Dot(b Vector) float64 { return (v.x * b.x) + (v.y * b.y) + (v.z * b.z) + (v.w * b.w) } func (v *Vector) Cross(b Vector) Vector { return NewVector( (v.y * b.z) - (v.z * b.y), (v.z * b.x) - (v.x * b.z), (v.x * b.y) - (v.y * b.x) ) }
sabotai is offline   Reply With Quote
Old 11-03-2021, 03:23 PM   #24
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
First, I changed the values over to float64 from float32 because the the functions in the "math" package use float64.

Secondly, in Go, public vs. private is based on the case of the name. Lower case = private, Upper Case = public. So in my vector struct, x, y, z, and w are all private, which means those are only accessible within the package. I like to keep things private unless I absolutely have to make them public, but for a Vector I also know I'll probably need to know the values at some point, so I threw in so "getters".

Magnitude returns the total length of the Vector
Normalize returns the the vector with its magnitude set to 1
Dot returns the Dot product of two vectors, useful for determining the angle of two normalized vectors.
Cross returns the cross product, a vector that is perpendicular to the two vectors. Order is important, a.Cross(b) returns the opposite value from b.Cross(a).


And that's that for the Vector class. Next up, Color and creating a Canvas
sabotai is offline   Reply With Quote
Old 11-04-2021, 03:33 PM   #25
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Second take on the vector package. To me, a.Dot(b), a.Cross(b), etc. looks weird. I changed most of methods to functions and now here is how the vector.go file looks like. I'm just used to making everything methods because of C#, but doing it this way makes the Go code that calls these functions look better IMO.

Code:
package vector import "math" type Vector struct { x, y, z, w float64 } func (v *Vector) X() float64 { return v.x } func (v *Vector) Y() float64 { return v.y } func (v *Vector) Z() float64 { return v.z } func (v *Vector) W() float64 { return v.w } func NewVector(x, y, z float64) Vector { return Vector{ x: x, y: y, z: z, w: 0.0 } } func NewPoint(x, y, z float64) Vector { return Vector{ x: x, y: y, z: z, w: 1.0 } } func ZeroVector() Vector { return NewVector(0.0, 0.0, 0.0) } func ZeroPoint() Vector { return NewPoint(0.0, 0.0, 0.0) } func Add(v1, v2 Vector) Vector { return Vector{ x: v1.x + v2.x, y: v1.y + v2.y, z: v1.z + v2.z, w: v1.w + v2.w, } } func Subtract(v1, v2 Vector) Vector { return Vector{ x: v1.x - v2.x, y: v1.y - v2.y, z: v1.z - v2.z, w: v1.w - v2.w, } } func Negate(v Vector) Vector { return NewVector(-v.x, -v.y, -v.z) } func Multiply(v Vector, value float64) Vector { return Vector{ x: v.x * value, y: v.y * value, z: v.z * value, w: v.w * value, } } func Divide(v Vector, value float64) Vector { return Vector{ x: v.x / value, y: v.y / value, z: v.z / value, w: v.w / value, } } func Magnitude(v Vector) float64 { val := (v.x * v.x) + (v.y * v.y) + (v.z * v.z) return math.Sqrt(val) } func Normalize(v Vector) Vector { return Divide(v, Magnitude(v)) } func Dot(v1, v2 Vector) float64 { return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z) + (v1.w * v2.w) } func Cross(v1, v2 Vector) Vector { return NewVector( (v1.y * v2.z) - (v1.z * v2.y), (v1.z * v2.x) - (v1.x * v2.z), (v1.x * v2.y) - (v1.y * v2.x) ) }


Now, when I call these functions, it'll look like this:

Code:
// This looks better v3 := vector.Add(v1, v2) // than this v3 := v1.Add(v2)

Last edited by sabotai : 11-04-2021 at 03:38 PM.
sabotai is offline   Reply With Quote
Old 11-04-2021, 03:38 PM   #26
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
And now for the Color package

Code:
package color type Color struct { r, g, b float64 } func (c *Color) R() float64 { return c.r } func (c *Color) G() float64 { return c.g } func (c *Color) B() float64 { return c.b } func NewColor(r, g, b float64) Color { return Color{ r: r, g: g, b: b } } func Add(a, b Color) Color { return Color{ r: a.r + b.r, g: a.g + b.g, b: a.b + b.b, } } func Subtract(a, b Color) Color { return Color { r: a.r - b.r, g: a.g - b.g, b: a.b - b.b, } } func MultiplyScaler(a Color, m float64) Color { return Color { r: a.r * m, g: a.g * m, b: a.b * m, } } func Multiply(a, b Color) Color { return Color { r: a.r * b.r, g: a.g * b.g, b: a.b * b.b, } }
sabotai is offline   Reply With Quote
Old 11-05-2021, 03:39 PM   #27
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Now things get interesting as I am getting some output from the program. The Canvas package outputs to a PPM file, which is a text-based image file format.

The first test of the output was created by having a projectile fly through the "world". On each update, the program put a red dot on its position. Here's the final image (loaded into GIMP and converted to a JPEG). It looks lopsided because I simulated a bit of wind as well. And the curve isn't 100% smooth because there's some rounding in the math to convert position (float64) to pixel location (int).



So there it is. We now have a canvas object holding pixel data and outputting the image to a file!

Last edited by sabotai : 11-05-2021 at 04:00 PM.
sabotai is offline   Reply With Quote
Old 11-05-2021, 03:43 PM   #28
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
And here is the canvas code

canvas.go
Code:
package canvas import ( "RayTracer/color" ) type Canvas struct { // first array = height (num of rows), second array = width (num of cols) pixels [][]color.Color } func (canvas *Canvas) Width() int { // Bail if there is no first array if canvas.Height() == 0 { return 0 } return len(canvas.pixels[0]) } func (canvas *Canvas) Height() int { return len(canvas.pixels) } func NewCanvas(w, h int) Canvas { canvas := Canvas{} canvas.pixels = make([][]color.Color, 0) for i := 0; i < h; i++ { temp := make([]color.Color, w) canvas.pixels = append(canvas.pixels, temp) } for idx0, _ := range canvas.pixels { // Height for idx1, _ := range canvas.pixels[idx0] { // Width canvas.WritePixel(idx1, idx0, color.NewColor(0.0, 0.0, 0.0)) } } return canvas } func (canvas *Canvas) WritePixel(x, y int, color color.Color) { if y > canvas.Height() { return } if x > canvas.Width() { return } // First index is Height (or Rows), so it goes [y][x] canvas.pixels[y][x] = color }

ppm.go
Code:
package canvas import ( "RayTracer/color" "fmt" "os" ) func ToPPM(canvas Canvas, fname string) { file, err := os.Create(fname + ".ppm") if err != nil { return } defer file.Close() file.WriteString("P3\n") w_and_h := fmt.Sprintf("%v %v\n", canvas.Width(), canvas.Height()) file.WriteString(w_and_h) file.WriteString("255\n") linesplitnum := 0 for i := 0; i < canvas.Height(); i++ { linesplitnum = 0 for j := 0; j < canvas.Width(); j++ { r, g, b := color.To256(canvas.pixels[i][j]) rgb := fmt.Sprintf("%v %v %v ", r, g, b) linesplitnum = linesplitnum + len(rgb) if linesplitnum > 70 { file.WriteString("\n") linesplitnum = 0 } file.WriteString(rgb) } file.WriteString("\n") } }
sabotai is offline   Reply With Quote
Old 11-05-2021, 03:54 PM   #29
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Some notes about the code

Code:
return len(canvas.pixels[0])

In Go, the len() function returns the length of what is passed to it, and in the case of multidimensional arrays, it gives the length of the "outer" most array. So len(canvas.pixels) gives the length of the first array. To get the len() of the second array, I have to pass it a reference to an index of the first array.


The ToPPM function is a bit of a mess. I need to clean that up. My IDE (I'm using JetBrains GoLand) is yelling at me about unhandled errors because I'm calling file.WriteString, file.Close, etc. without storing the return values. I'll get there.

linesplitnum - Some software that uses PPM can't handled lines longer than 70 characters (or so says the author) so I made sure to add a \n every time I'm about to go over 70 characters in a line


defer file.Close()

In Go, you can defer a function call, block of code, etc. until the function is done. In this case, I defer the file.Close() call until the function is finished. It'll be called when the the function returns to the caller.

I really like this feature. In my function as it is now, there's only one path to return, so I could just add it to the end of the function. However if there were multiple return points, I would need to make sure to close the file at each one. Instead, I defer, and file.Close() is called when the function returns, closing the file.

Last edited by sabotai : 11-05-2021 at 03:56 PM.
sabotai is offline   Reply With Quote
Old 11-05-2021, 04:03 PM   #30
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
And next up are two whole chapters on Matrices.....hurray...
sabotai is offline   Reply With Quote
Old 11-08-2021, 09:55 PM   #31
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Did the first of the two chapters on matrices. My first pass at it, I created multiple structs, Matrix2, Matrix3, Matrix4 for the three sizes I will need. And for Matrix2 and Matrix3, the functions are few as Matrix4 will be, by far, the most used struct. I wrote out all the tests, and the they all passed.

I wasn't happy doing that. I wanted one struct that supports variable sized matrices. I spent a few hours doing that today. Just one struct, Matrix, and one version of each function..

Writing functions to support variable sized Matrices was a pain, though. Lots of if...else statements, one function, calculating the Determinant, is different for 2x2 matrices than for 3x3 and 4x4 matrices. Had to make most of the functions to be able to return an error for certain cases. It was just very messy.

So even though having Matrix4, Matrix3 and Matrix2 is an inelegant solution, the functions are very clean. So I'll go with that.

Last edited by sabotai : 11-08-2021 at 09:55 PM.
sabotai is offline   Reply With Quote
Old 11-09-2021, 03:29 PM   #32
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Cleaned up the testing and matrix code a bit finally set up a GitHub for it.

GitHub - JayFromNJ/GoRayTracer
sabotai is offline   Reply With Quote
Old 11-11-2021, 03:09 PM   #33
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love


I spent the last few days finishing up the matrix chapters. Still not 100% happy with it. Have some parts in the test where I convert the vector to a [4]float64 and on the next line, convert that [4]float64 into a new vector. Whatever, it works, moving on.

The image above was created by taking a point and applying a rotation matrix to it to form a "clock".

The next chapter is called Ray-Sphere Intersections, and it's a big one. The chapter after is "Light and Shading", another big one but by the end of that chapter, I'll have a sphere rendered from ray tracing.

Last edited by sabotai : 11-11-2021 at 03:15 PM.
sabotai is offline   Reply With Quote
Old 11-27-2021, 05:24 PM   #34
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Finally finished chapter 5, Ray-Sphere Intersections. I have the objects checking if a ray intersects with itself. Next on to Light and Shading and that's when the interesting results really begin.

I did learn that Go does not allow circular importing. IOW, package "ray" can not import package "object" and also have package "object" import package "ray"

I like that, I think it's better, it always felt weird to me when I would do it in C#, but I must have gotten quite used to it because it caused a bit of an issue for me in Go and I couldn't have object and ray import each other. I got it sorted out, but that's certainly something I will have to keep in mind going forward.
sabotai is offline   Reply With Quote
Old 12-04-2021, 05:15 PM   #35
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love


And we have 3D output!

I feel like the project has become a bit of a mess so I'm going to stop and refactor the code and rewrite some of the tests before moving on. The github is updated if you want to see my shitty code.

The next chapter is called "Making a Scene" and is when I'll start making a world with multiple objects and creating a camera object.
sabotai is offline   Reply With Quote
Old 02-27-2022, 03:23 PM   #36
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
A short update

I chipped away a little bit here and there on the ray caster, but I'm done with Go for now. I got a basic idea of what working in it would be like and I'm ready to sample the other two languages.

For Rust, here is what I'm doing:

Videos:
Plurasight - Rust Fundamentals. This course was just added at the end of January is just goes over the basics. I'm watching through it now.
Udemy - The Rust Preogramming Language - Already watched this awhile ago and is the same as the pluralsight course (and probably a bit outdated) .
Udemy - Learn Rust by Building Real Applications - a course on learning Rust by making a simple HTTP Server.

Books:
Hands-On Concurrency with Rust

I have a few other books I got through Humble Bundles, but they're the same as the video courses. Just a run down of the language's basics and features.



For Scala, I don't have much outside of Pluralsight, which has a good number of videos on it. But my subscription ends at the end of April and I don't plan on renewing it. I'm going to be on a much stricter budget soon. Plus I find most of Pluralsight's content really dry and boring. I'll be on the look out for any Humble Bundles with Scala books in them.
sabotai is offline   Reply With Quote
Old 03-09-2022, 04:34 PM   #37
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
Humble Bundle has a bundle full of No Starch Press books. I am a fan of this publisher (O'Reilly is usually good too, but Packt is very hit or miss).

The bundle is called "The Joy of Coding". I have most of the books in the bundle. I usually grab their bundles and, of course, they repeat books a lot. But the cost of the bundle is worth it to grab the few new ones.

For me, the new ones are

Rust of Rustaceans (a 2nd level book - a sorta vol.2 to The Rust Programming Language book from them)

Computer Graphics from Scratch (create two renderers, a raytracer and a rasterizer - uses psuedocode so can be used for just about any language)

Network Programming with Go

and a few others I probably won't ever get to.
sabotai is offline   Reply With Quote
Old 04-19-2022, 05:19 PM   #38
sabotai
General Manager
 
Join Date: Oct 2000
Location: The Satellite of Love
An update to this is that I am finally done moving (as of 2 weeks ago) and am settling into my new home. In the last week, I finished off the Pluralsight course for Rust Fundamentals. Next up, the 2 No Starch Press books (The Rust Programming Language and Rust for Rustaceans).

In other news, today I went through a stack of old hard drives to see if there's anything on them that I want to keep before I finally destroy and dispose of them. I've done this 2 or 3 times before, so what's really going to happen is that they will stay there for a year or two before I again look through them before planning to dispose of them.

Every time I do this, I'm on the look out for a few specific things. My old Delphi project for a football sim I worked on way back in the early 2000s, my old Tennis sim in C++ and an old boxing sim I worked on in the late 90s in Pascal.

Today, I found them. I have no idea how I missed them until now, but I found them. The oldest file was a fairly mature version of the boxing sim that's dated 10/5/1996. Less than a month after I started college. I also made a basketball sim in Pascal in high school but that is lost to history.

Will I share the code? LOL, hell no. I don't want anyone knowing I was that bad of a coder. In my defense, I didn't have any books on Pascal when I learned. Just whatever I could find on BBSs, which was mostly just sample code. I learned mostly through trial and error, and pestering a few people on those BBSs with questions.

Global variables.....global variables everywhere....

I spent a good hour or two strolling down memory lane trying to make sense of some of my truly awful code. Maybe I'll revive the boxing sim in Rust as a project.
sabotai is offline   Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is On
Forum Jump


All times are GMT -5. The time now is 01:55 AM.



Powered by vBulletin Version 3.6.0
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.