Closed ianterrell closed 1 year ago
Perfect! I put your note about the board / renderer on it first. I think we would also need either a Player or a Team class. What do you think?
and make it show up? sweat_smile
As far as making the board appear, I kind of threw this together. I don't know if it's close enough to your vision, or if I misinterpreted how the Curses class should function. I shortened the class name just for brevity's sake during this spitball session. What do you think?
class Curses
def display(coords)
coords.each {|row| puts row.join} # For each element in coords, join as a string and output it with a new line.
end
end
class Board
def initialize(rows,columns)
@rows = rows
@columns = columns
end
def grid
Array.new(@rows) {Array.new(@columns, "β¬ ") } # Make 2D array (for each @row, a square is printed @columns times).
end
end
Proof of concept:
irb(main):017:0> board = Board.new(4,4)
=> #<Board:0x00007fc23bad9c60 @columns=4, @rows=4>
irb(main):018:0> renderer = Curses.new
=> #<Curses:0x00007fc23bae4070>
irb(main):019:0>
irb(main):020:0> renderer.display(board.grid)
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
This is among the hardest parts βΒ there are a million ways to do things, what do we pick? We can go however you like, but here's a bit about what I picture mentally.
def grid
Array.new(@rows) {Array.new(@columns, "β¬ ") } # Make 2D array (for each @row, a square is printed @columns times).
end
To my mind, that might already be mixing data and display too much. I would call the β¬
a display level concern. At a minimum, it's terminal specific. To elaborate on what I mean by data and display, an ideal would be to be able to take the non-display types and reuse them without modification in a version of the game rendered with, say, sprites in DragonRuby.
coords.each {|row| puts row.join}
If you like, we can try it with curses
where we won't use puts
much and will instead use its methods to draw at particular locations on the screen. That'll let us more easily do stuff like use the arrow keys to select a cell to paint.
So I might visualize the initialization as something like:
# Board
def initialize(rows, cols)
@rows, @cols = rows, cols # if useful to keep
@grid = Array.new(rows) {Array.new(cols, ?) }
end
The ?
is what's going to represent the state of that board location. Is it a symbol like :player
vs :npc
? Is it an index of a player in an array? Is it a Cell
type that can expand to more information? (ooh, like, the turn number it was painted so that after N turns the paint has dried and is permanent). So that's something to decide.
I think we would also need either a Player or a Team class. What do you think?
We probably do want a Player
class eventually. What data to track with it? Presumably whether it's a human player or the AI at least... although that said, if it's a single player game we may not need a Player
type. It could probably go any direction there.
I might suggest we start with a game that's not really a game and won't be any fun:
That lets us flesh out first drafts of:
After that, perhaps we add a computer randomly selecting a cell to paint; still not likely fun, but a start.
Once those pieces are working, I imagine it would be way faster to experiment and iterate on the actual game parts.
What do you think?
I might suggest we start with a game that's not really a game and won't be any fun.
The road map you put together sounds great! I added it to the wiki.
Once those pieces are working, I imagine it would be way faster to experiment and iterate on the actual game parts.
That makes sense to me. Small increments is a good idea, and will definitely help me learn along the way. I want to internalize some good practices.
To my mind, that might already be mixing data and display too much. I would call the β¬ a display level concern
That makes sense to me! Since we haven't decided how to colorize the squares, I'm probably getting ahead of myself but: What if we defined some preset squares for the CursesBoardRenderer class to pick from depending on the coordinates' states?
#CursesBoardRenderer
def empty_square
"β¬ "
end
def red_square
Rainbow("β ").red
end
def blue_square
...
ooh, like, the turn number it was painted so that after N turns the paint has dried and is permanent
I like that idea too! I'm adding it to the wiki. The player will need a cue to see which squares are almost dry -- what if they blinked between empty and the new color for a few loops? The Rainbow gem is also capable of handling that.
That makes sense to me! Since we haven't decided how to colorize the squares, I'm probably getting ahead of myself but: What if we defined some preset squares for the CursesBoardRenderer class to pick from depending on the coordinates' states?
Sure, that could work fine. At this point there's about no wrong way to do it. If we keep the display pretty separate from the rest, we can try out several different renderers even. One could do the square approach:
β¬ β¬ β¬
β β β¬
One could draw with box drawing characters:
βββββ¬ββββ
β β β
βββββΌββββ€
or whatnot. (But now that I type out the box drawing characters I remember how much of a pain they can be and how much I dislike them sometimes. :))
But alas! Yes, let's start with this version:
β¬ β¬ β¬
β β β¬
Okay, so planning out this first step with a few more sub-steps:
q
as input for now to quitDo you want to take a first pass at some of that? Would you like me to? I think the navigation demo I built with curses can work as an outline in some ways.
start.rb
. I think it satisfies Steps 1 and 2 while retaining our goals, but let me know if it doesn't!I modified my first idea for initialization based on your suggestion to keep the Board instance in charge of mechanics, and the Curses instance in charge of rendering. I also updated it to more closely resemble what you wrote.
That said, I couldn't figure out how to work @grid
into a solution. So, I opted to keep grid
an instance method instead, but that doesn't mean I think it's better-- I'd love to see what you would have done differently. Let me know π
erikamaker@erikas-computer:~/war-paint$ ruby start.rb
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
Select `Q` to Quit >> I don't want to quit yet.
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
β¬ β¬ β¬ β¬
Select `Q` to Quit >> q
erikamaker@erikas-computer:~/war-paint$
Very very briefly 'cause I'm up too late. :)
Thanks for getting started on that!
It'll be good practice to work in branches and create PRs βΒ let me know if there's still difficulty there. For that work it could be some workflow like...
git checkout -b feature/start-rendering
# ... do work
git add .
git commit # etc
git push -u origin feature/start-rendering
In one PR I reorganized the code into one file per class inside lib
and the binary to run it in bin
. This is... somewhat standardish and sort of mirrors aspects of gems and of Rails projects. There's no official way things have to be done though, but this can help things work by mirroring conventions.
I generally think it's helpful to think about organizing code that way.
So the first PR has some refactors that basically just move your code around and organize it a little bit according to my liking. Again, opinion based. But please review and add comments and let's discuss anything I did there that's interesting or stands out to you. You can add comments directly to the diff in the PR. Just click the line number:
The second PR adds a renderer based on the Curses library, which is re-rendering the screen on each pass. It's got arrow key selection and not much else. But it's pretty neat! (to me)
Again, please look through and add comments and questions.
The neat thing about separating data and presentation is that we can have multiple renderers!
I might spend 3 more minutes adding configurable rows/cols in a new pr. and then sleep.
Thanks for getting started on that!
Of course! Thanks for the PRs and updates. I'm going to work on digesting the changes you made. I've merged the one and left the other two open for discussion.
It'll be good practice to work in branches and create PRs β let me know if there's still difficulty there.
I'll also practice more git branch management before I commit anything new. I learned a little bit, but there's some anxiety about working in other branches and making pull requests. I understand it on a higher level, but in practice I get "stage" fright.
There's no official way things have to be done though, but this can help things work by mirroring conventions.
Is there a boilerplate for this? This is something I found but wasn't sure about:
There might be a day or two lapse while I'm also working on some Odin modules (css, HTML, and javascript this week), and the Ruby text. I'll be back soon π
I'll also practice more git branch management before I commit anything new. I learned a little bit, but there's some anxiety about working in other branches and making pull requests. I understand it on a higher level, but in practice I get "stage" fright.
Don't stress, and don't let it slow you down from writing any code you want to. It's important to learn if you want to work in software development, but it's just a tool. Practice makes progress. You're learning a ton of different things all at once*; if it were me it would be bound to be overwhelming sometimes. Plus job hunting? How's that going?
(*I've read some things that say learning different topics at once might be easier! Or, rather, you have a saturation point where within a given time frame you can only learn so much on a given topic, but independent topics don't seem to influence each others' saturation points. I'll have to look that up again.)
Is there a boilerplate for this? This is something I found but wasn't sure about:
The *.gemspec file gives it away as a gem. You can create one with bundler via bundle gem <gem-name>
, and it will walk you through some options and then generate a filesystem structure like the above.
That's roughly what I was pattern matching off of, although there are a few differences. I've also been writing Rust lately and their packages (crates) look similar.
There might be a day or two lapse while I'm also working on some Odin modules (css, HTML, and javascript this week), and the Ruby text. I'll be back soon π
There's no rush!
It's hard for me to quantify the level of attention I put towards something. I'm always either focused or daydreaming, with very little in-between, oscillating all day for about 12 hours. I don't feel like I'm doing enough for that reason, but I don't want to burn out this time. And anyway, I figure small steps are still steps.
Plus job hunting? How's that going?
I was told that I can cover anyone on maternity leave during 2023, so it sounds like I'll have (partially) consistent employment through the year. But, I am slowly applying for other work too. I updated my resume recently and have gotten some bites, usually things like "We're impressed but ultimately..." type emails.
You can create one with bundler via bundle gem
, and it will walk you through some options and then generate a filesystem structure like the above.
That's awesome, thank you. I know that gems usually provide some sort of extension or library that are useful in building other Ruby programs (but let me know if I'm wrong). I was curious what other ways Ruby programs are packaged. Is there such a program bundled as a gem that shouldn't be?
And anyway, I figure small steps are still steps.
At work I'm known for saying, "baby steps." 12 hours is definitely too much to focus on something. :)
I was curious what other ways Ruby programs are packaged. Is there such a program bundled as a gem that shouldn't be?
I've seen CLI tools packaged as gems, and of course libraries are packaged that way. Other than that my only experience with Ruby programs is with Rails, and those aren't really packaged at all outside of the repo. I know some tools wrap Ruby programs up as executables in an OS's trappings; that's how I'd distribute a non-CLI general purpose app I suppose.
I've always had such a "all or nothing" mindset since I was young, so baby steps is definitely a healthy change. It's more conducive for learning.
After merging your changes, the only issue I'm noting is that we can't use "Q" to quit for the latest rendering, so I unchecked it. Is that the same on your end?
After merging your changes,
I think some of it was partially merged; I've just pushed to main with all my updates from last weekend.
the only issue I'm noting is that we can't use "Q" to quit for the latest rendering, so I unchecked it. Is that the same on your end?
my first bug on the project! :)
I expect that you're typing a capital Q and I had been checking for a lowercase q out of habit.
Want to try to fix that and take a stab at the remaining checkboxes?
Also, want to focus on the curses renderer or should we keep the other terminal one around also?
I practiced some branch management (creating a new branch, committing changes, creating pull requests for them, merging them). I put it under a test folder-- it seems correct, but let me know if anything's off (I included my CLI i/o).
my first bug on the project! :)
Did you fix it? I'm not seeing any changes to the files, but can't replicate the bug. In fact, lowercase and uppercase 'Q' both work just fine. I wonder why it didn't work the first run. In any case, I re-checked that part off Iteration 1 π
EDIT: This might just be due to my most recent comment-- possibly old version. Un-checking until I can confirm.
Also, want to focus on the curses renderer or should we keep the other terminal one around also?
The curses renderer is such a useful tool, and I'd like to get better at implementing it. I read through its documentation some-- damn, there is a lot. Some assets feel intuitively named, but others are like Greek to me. I find myself knowing what effects I'd like to apply to a terminal, but not how to search the documentation for it. Getting a baseline for the tool would be cool.
I'm going to start work on letting spacebar
or return
paint a square-- if you haven't already started that! π
Sorry for the delay-- I ran into a snag. Working on getting the Curses version of the board to run again-- the first version keeps running instead. I wanted to test an idea I had for painting the cells, and noticed we still had the old terminal rendering file in lib
, so I deleted it as well as require_relative
. That threw some errors, so I restored them for now, but...
Any idea why rake run
would give the output for our older version of the board despite pulling from main? My files look the same as remote's.
Did you fix it? I'm not seeing any changes to the files, but can't replicate the bug. In fact, lowercase and uppercase 'Q' both work just fine. I wonder why it didn't work the first run. In any case, I re-checked that part off Iteration 1 π
No, I haven't changed anything. My analysis was a guess. I just ran it and it works for me just fine, too.
EDIT: No, apparently I had changed something locally that evening to get it working with capital letters, and I didn't even remember. π
- if key == 'q'
+ if key == 'q' || key == 'Q'
The curses renderer is such a useful tool, and I'd like to get better at implementing it. I read through its documentation some-- damn, there is a lot. Some assets feel intuitively named, but others are like Greek to me.
It's neat and I like it but it's definitely not always intuitive for sure. The underlying library is an old C library and so follows C conventions, and the Ruby is just a thin wrapper on top. It doesn't look like Ruby exposes every feature either, although I expect it exposes enough for us to work with.
Any idea why rake run would give the output for our older version of the board despite pulling from main? My files look the same as remote's.
rake run
just executes ./bin/war_paint
:
I configured the executable to only run the curses based renderer if passed the short flag -x
or long flag --curses
:
so rake run
is just doing the existing version since it does not pass the flag.
I left the other renderer you started in place so that I didn't just rip your work out from underneath you. :)
So there are a few options that come to mind to fix that: adjust the Rakefile, adjust the options defaults, trim to a single renderer, or just run it manually via ./bin/war_paint --curses
.
If you want to consolidate on the curses render, I think the steps you took deleting the other file and the require line are right; but you'll also need to adjust the executable script in bin
, as well as maybe remove the option configuration flag --curses
since it will be the only renderer and doesn't need explicitly selected.
I've done a bit of throwing you in the deep end with all of this βΒ I hope it's fun and helpful in some ways? :)
- if key == 'q'
+ if key == 'q' || key == 'Q'
I've run into that before! I usually downcase
the input so it's always checking for lowercase values anyway. There is literally no reason for me to share this, I'm just flexin where I can π
you'll also need to adjust the executable script in
bin
If I remove the --curses
flag, wouldn't I just need to remove || Terminal Renderer
from:
# bin
renderer_class = options[:renderer] || TerminalRenderer
I've done a bit of throwing you in the deep end with all of this β I hope it's fun and helpful in some ways? :)
It's a challenge for sure. But, I can tangibly list the things I'm learning along the way, so it's both helpful and fun. So, today I'm going to experiment a little and work on
If I remove the --curses flag, wouldn't I just need to remove || Terminal Renderer from:
You'd probably restructure the whole thing as the renderer_class
intermediate variable is no longer necessary. I think the advice here is the general make sure you understand each part of each line, and then decide what each should do.
today I'm going to experiment a little and work on ...
Yay, fun! Try your work in a branch and then rather than pushing to main
push to another remote branch and create a PR that I can peek at. I'll be curious to see what you come up with!
Got the appropriate program running using the manual path ./bin/war_paint --curses
.
I think the advice here is the general make sure you understand each part of each line, and then decide what each should do.
I added comments to each file to make sure I really understood how our program worked. A little bit of it (mostly script stuff) is a little over my head, but I think going through each line like that helped. I merged these comments.
- Removing the TerminalRenderer and any snippets that might trigger an error / 2. Getting the program to run again
Done. Commenting through the code helped me understand what needed to change after the file deletion. Thanks for the recommendation I take a step back π
I merged the changes. I'll work on the painting feature when I have some more time today.
Got the appropriate program running using the manual path ./bin/war_paint --curses.
π also with your changes since CursesRenderer
is the only one, rake run
works to load that version. I filed a PR for you slightly tweaking the bin script.
I added comments to each file to make sure I really understood how our program worked. A little bit of it (mostly script stuff) is a little over my head, but I think going through each line like that helped. I merged these comments.
Do what you need to do to make sure you are learning and understanding things as you like, but in general I would caution you against making such comments a habit. Generally we strive for "self-documenting code," and (eventually) comments should be reserved for only things that are very difficult to communicate within the code itself.
It's sort of like reading a play of Shakespeare's with the Cliff's Notes inline: for a few parts they are really helpful and you are happy to see them, but on average they sort of get in the way. They're also less and less helpful as you familiarize yourself with the text.
Thanks for the recommendation I take a step back
It's been said that programming is something like 10-20% writing code and 80-90% reading code. It's close to true!
What text editor do you use? You may want to check for a setting about trimming trailing whitespace. It's a common practice, and I think there's a bunch of it added in the files.
Although text editor settings are (arguably) arbitrary, it is pragmatic to align them. Otherwise, diffs can become very messy as non-important changes get pushed at the same time as meaningful ones. e.g. below there are no non-whitespace changes! But it's hard to see that; real changes could be hidden in those lines and you'd not be able to tell at a glance.
I'll work on the painting feature when I have some more time today.
Let me know if I can help! If you like I can probably point you in a few directions, or give feedback on what you're planning before or after you write it.
I merged it! That makes sense. I left a comment in the merge confirmation, but is it better to keep communication in the issues? I don't want to scatter our communication too much.
but in general I would caution you against making such comments a habit
That makes sense. I'll work on making my code readable on its own, and if I need a logic flow for my own benefit, I can just make one in my local environment.
They're also less and less helpful as you familiarize yourself with the text.
Regarding Cliff's Notes: that's an interesting analogy I hadn't had before. I wonder if that's related to how I sometimes over-focus on the small details and miss the grander picture until later. It's hard to have context if you're focusing on every word, rather than the sentence.
What text editor do you use? You may want to check for a setting about trimming trailing whitespace. It's a common practice, and I think there's a bunch of it added in the files.
I've been using VSCode. I just turned that setting on-- not something that I've come across yet. Thank you for illustrating the issues that trailing whitespace can cause π
Let me know if I can help! If you like I can probably point you in a few directions, or give feedback on what you're planning before or after you write it.
That honestly sounds really nice. After I got it working again and commented through the program, I started testing my ideas and realized they wouldn't work.
At one point, I managed to make a constant for the "paint" key (put under the movement keys). I also made a temporary painted_box = ["XXX","XXX"]
and tried adjusting the ternary expression for outputting either selected_box
vs unselected_box
, but wasn't sure how to do that without shoehorning it in. I also considered adding an instance variable to toggle whether empty
was true in the Cell class.
So I guess, smallest steps first, where would you recommend I start? I reverted the changes I made in my local branch for now.
I left a comment in the merge confirmation, but is it better to keep communication in the issues? I don't want to scatter our communication too much.
I get emails for almost everything of importance on GitHub, including comments, so I generally won't miss anything even if it gets scattered.
At work, I use comments and discussions on PRs to have focused discussion on certain bits of code. Generally when I've been putting code snippets in these comments to discuss, if they were in an open PR I'd comment on the lines in question directly.
Issues are good for bigger picture things outside the context of specific changes to code IMO; general strategy, logistics, planning, etc.
Regarding Cliff's Notes: that's an interesting analogy I hadn't had before.
My daughter was struggling through Romeo and Juliet so I got her the notes recently. π Only reason it came to mind.
Some of it is vocabulary, a lot of it is just practice. Another analogy might be reading in a foreign language. At first it's helpful to write down the translation beside a word; later that might be noisy but it remains helpful to have a translation dictionary nearby for reference; eventually you see attr_reader
or .map
and since you're fluent they just blend in.
smallest steps first, where would you recommend I start?
It sounds like you're on the right path in how you're thinking, generally.
At one point, I managed to make a constant for the "paint" key (put under the movement keys).
That sounds right for now. Something analogous to MOVEMENT_KEYS
and movement?
seems fine. I think at some point β maybe right after this iteration βΒ we may want to refactor this approach, but for now it's simple enough I think that's fine.
I also considered adding an instance variable to toggle whether empty was true in the Cell class.
I think that's probably right βΒ eventually we'll need to track who has last painted the cell, or similar, but for just the first bit I think we can just use a boolean. Maybe @painted
, where new cells are initialized as not painted and cells are considered empty if not painted. Although empty?
is funny here; maybe it wants to be painted?
instead.
I also made a temporary painted_box = ["XXX","XXX"]
We could keep the boxes as they are for now and add color for paint! If we took that strategy then the shoehorned three choice becomes two distinct easy choices:
selected?
painted?
Curses is a C library so it's weird. But it's just a matter of learning how it speaks. You have to pick a foreground color and a background color, and then turn on that color pair. Here's an example you can run:
require "curses"
Curses.init_screen
Curses.start_color
Curses.curs_set 0
UNPAINTED = 100
Curses.init_pair(UNPAINTED, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
PAINTED = 101
Curses.init_pair(PAINTED, Curses::COLOR_WHITE, Curses::COLOR_RED)
screen = Curses.stdscr
screen.setpos(0, 0)
Curses.attron(Curses.color_pair(UNPAINTED))
screen.addstr("Unpainted")
screen.setpos(1, 0)
Curses.attron(Curses.color_pair(PAINTED))
screen.addstr("Painted")
screen.refresh
sleep(5)
Curses.close_screen
Key methods to look up are:
init_pair
color_pair
attron
The only other advice on this topic I might point you toward is letting the renderer communicate with the board for painting. Something like @board.paint!(@selected)
.
Does that help you get started?
if they were in an open PR I'd comment on the lines in question directly... Issues are good for bigger picture things outside the context of specific changes to code IMO; general strategy, logistics, planning, etc.
Good to know. Adding it to my growing notes on git.
My daughter was struggling through Romeo and Juliet so I got her the notes recently. laughing Only reason it came to mind.
I still remember reading that when I was a freshman in high school! Crazy to think of how long ago that was now.
eventually you see attr_reader or .map and since you're fluent they just blend in.
That's my favorite part about learning any subject at all, that feeling. But, sometimes I'm frustrated by how slow the process can feel π
It sounds like you're on the right path in how you're thinking, generally.
Okay! I added PAINT_KEYS = [Curses::Key::ENTER]
(I kept it an array in case we wanted to add another key--I couldn't find the space bar in the documentation for Curses). I also gave Cell an initialization method that assigns false to @painted
.
Key methods to look up are...
Quick question about init_pair
: From what I read, it sounds like the number value is arbitrary, with some conventions. Curious why you picked 100 and 101. Maybe I'm overthinking that.
color_pair
makes pretty good and easy sense to me. It just returns the paired colors with the values I asked about a second ago.
attron
took a second to wrap my head around, but I think it's not unlike CSS stylizing HTML. It sets the attribute of the displayed text to the color pairs that we predefined.
Does that help you get started?
It does a bit, yes. I played around for a couple hours trying to get the changes to work. So far I haven't /broken/ anything, but it's not colorizing when I hit ENTER
. I'm going to work more on it tomorrow before I try to merge anything, and I'm giving my brain a little break now that it's past midnight π
I kept it an array in case we wanted to add another key--I couldn't find the space bar in the documentation for Curses
It looks like there isn't a defined constant, but rather just the space character: ' '
.
Quick question about init_pair: From what I read, it sounds like the number value is arbitrary, with some conventions. Curious why you picked 100 and 101. Maybe I'm overthinking that.
Very observant! I picked them mostly arbitrarily. In my terminal engine I'm writing I've had trouble overwriting color definitions with numbers < 16, despite the documentation saying that I should be able to. And color pair 0 is reserved. I just checked my notes to confirm. But when I sketched out that sample it was late and I was tired and I couldn't remember if I had trouble with colors or with pairs or what the threshold was, so I just picked something higher than 64. π
attron took a second to wrap my head around, but I think it's not unlike CSS stylizing HTML. It sets the attribute of the displayed text to the color pairs that we predefined.
Yes βΒ applied to subsequent calls to output strings.
I played around for a couple hours trying to get the changes to work. So far I haven't /broken/ anything, but it's not colorizing when I hit ENTER. I'm going to work more on it tomorrow before I try to merge anything,
So, this is the benefit of branches and pull requests. You can push code up to a branch even if it doesn't work, and it doesn't hurt anything. You can create a pull request without the intent of merging it to main
immediately (there's even a draft feature now), and you can discuss it in context.
In short, you could push up your code to a new branch, create a PR, add a comment to a line that says, "I'm trying to do X here, but instead of X I see Y, what do you think?" or similar.
Sometimes approaches don't pan out, and you just close the PR without merging and delete the branch and start again. Or you close the PR without merging, work on your branch some more, and open a new PR up later. Or you keep the PR up, iterate on the branch to incorporate feedback, and eventually merge it. Super flexible!
The changes I made are pretty small (though I did delete all of the comments that make it hard to read). I'm at a point where I can explain what each of our files do by the line, and have experimented with Curses a little bit. Though I made a pull request for the changes, I feel a bit stuck. I have one quick question:
require "curses"
Curses.init_screen # Having a hard time seeing why this is needed. It runs without?
Curses.start_color # Can't make color without this.
Curses.curs_set 0 # Removes the cursor, which we want.
UNPAINTED = 1 # I removed this constant and inserted the value in the init_pair parameter
Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLACK) # Seen here.
# Since doing that works, I'm just curious why you'd assign it to a constant first? Readability?
# I don't do it this way in the recent PR I made, just curious why it's done this way π
I figured the curses renderer is going to need to use unselected_box
and selected_box
for the string values, but I am having a hard time figuring out how to implement that. My (vague) idea is the play!
method is updated to include the PAINT_KEYS, and if that's triggered it'll execute the paint!
method within the render
class.
# paint!
@screen.attron(Curses.color_pair(PAINTED_SQUARE)) do
box = selected?(row_index, column_index) ? selected_box : unselected_box
end
I didn't even add this part yet, just feeling a little lost. Am I on the right track still?
Since doing that works, I'm just curious why you'd assign it to a constant first? Readability? I don't do it this way in the recent PR I made, just curious why it's done this way π
The constant is for readability, yes, in a few directions.
PAINTING
." init_pair(1, ...)
is a lot harder to connect to its purpose than init_pair(PAINTING, ...)
PAINTING
is likely to be more fruitful than searching for 1
Generally, you can think about programming as "layers of abstraction". Your screen is reading binary data off of wires to turn on lights, but that's hard to think about so once it's done we wrap it in some code; ... many layers in between ...;Β your terminal window is reading ANSI escape codes, but we don't want to think about that so we wrap it in the Curses library; Curses uses numbers but we don't want to think about those so we wrap them in human readable symbols.
Professional programming is the same. There is human language level specified functionality that we want to encode in our program; some parts of the code are going to be responsible for translating between the language of the business domain and the language of the computer.
We could go further than we're doing here! There's no limit to the creativity in APIs especially in Ruby. But there are balances to find.
I didn't even add this part yet, just feeling a little lost. Am I on the right track still?
Hmm! Maybe not, at least in how I interpret what we're doing. But I have a lot of assumptions baked into my mental model of how to proceed. Here's how I conceptualize these moving pieces:
You're along the way of separating data from presentation by adding @painted
in the Cell type. We want to track painted status there, and then use that information at the rendering phase.
The game is structured as a loop: render, read input, decide action to take, take that action, repeat. We have a few actions we can take:
Since all of these are separate concepts, we probably want to keep them as separate methods βΒ "chunking" again, clear responsibilities per type and method.
So the renderer's painting method probably calls a board method, like @board.paint!(row, col)
; that probably updates the cell either directly or indirectly.
That's all completely isolated from rendering! On the next render loop, we'll color the cell appropriately based on cell.painted?
and pick the box style to draw depending on selection.
At least, that's how I think about it. Here's an example of how it looks:
I've made these changes and put them in a PR; some notes:
Anyway, just some little things to think about! Let me know how else I can help.
We could go further than we're doing here! There's no limit to the creativity in APIs especially in Ruby. But there are balances to find.
Speaking of balances-- I sometimes find myself unsure if I'm abstracting something too much. At one point does extracting behavior into its own method for clarity's sake become more harmful than helpful? Is there a rule of thumb?
I've made these changes and put them in a PR
I took a few hours to digest the changes you made. I found that this PR helped the pieces click more easily in my head-- thank you for chunking it out like that, was really valuable. I did merge the changes-- but I'm not sure I understand what you mean by "new branch into [my] branch" π
Speaking of balances-- I sometimes find myself unsure if I'm abstracting something too much. At one point does extracting behavior into its own method for clarity's sake become more harmful than helpful? Is there a rule of thumb?
You'll see a few guidelines tossed around. I just the other day was re-exposed to someone's idea that every method should be 5 lines of code or fewer. You'll also come across the DRY principle (which is of extreme importance but often misunderstood), which is sometimes used as justification for a guideline to extract code to a method once you've written it 3 times.
To sum up my philosophy, I might invoke the idea of "self-documenting code." This is the idea that you don't really need comments because the code documents itself.
What does this do?
@board.grid.each_with_index do |row, row_index|
row.each_with_index do |cell, column_index|
draw_cell(cell, row_index, column_index)
end
end
Let's see, well, it looks like it loops through each row of the grid, maintaining its index, and then it loops through each item in the row, also maintaining that index, and then...
What does this do?
def draw_board
@board.grid.each_with_index do |row, row_index|
row.each_with_index do |cell, column_index|
draw_cell(cell, row_index, column_index)
end
end
end
Oh, it draws the board!
Then, only if and when you want to, you read it to find out how it does so.
Work caught me, and I've completely lost my train of thought.
But the guidelines for method extraction might be:
The last point can be broken down into:
Those are indications of complexity, which might warrant breaking down further.
The abstraction means sort of...
def render
draw_board
draw_instructions
@screen.refresh
end
That is all pretty much at one level of abstraction (maybe two). It's at a high level talking about it how we might discuss it in English. It's very much about the "what" and not about the "how". Whereas the method above actually drawing the board is at a different level of abstraction: it's more about the data structures and mechanics of it all.
but I'm not sure I understand what you mean by "new branch into [my] branch" π
PRs go from a branch to a branch.
When you merged it, it merged into erika_test
and not into main
. I've pushed it all to main now.
You'll also come across the DRY principle (which is of extreme importance but often misunderstood), which is sometimes used as justification for a guideline to extract code to a method once you've written it 3 times.
DRY was exactly why I wondered this, since I'd seen multiple variations on the expectation. When you say it's "used as justification", do you mean that to " extract code to a method once you've written it 3 times" isn't always a good idea? I'm trying to imagine a scenario where repeating oneself would be more efficient. Is this related to "prematurely optimizing"?
To sum up my philosophy, I might invoke the idea of "self-documenting code."
So, as long as it reflects clear intent and concise (as possible) syntax with good naming conventions?
When you merged it, it merged into erika_test and not into main. I've pushed it all to main now.
Oh, oops. I see what you mean now. That's embarrassing! Thank you π
I got good news that I won't be getting laid off-- another department wants to hire me. That's come with a lot of training (8 hour zoom sessions daily), so I've been mentally drained this week. I'm planning a screen-free weekend to regroup and start fresh on Monday.
That said though, I'd like to try item 5 before tomorrow evening! I'm sorry I haven't added much value to this repo yet, but watching you work has been really beneficial. This item looks like it should be simple! Side note: painting the cells is strangely addicting, like popping packing bubbles.
I'm planning on making an array comprised of all Cell instances, and to iterate over it with a block that checks their state. If they're all filled, I'll have it output something like "GAME OVER!" and sleep a second before quitting. Hopefully that idea works! I'm adding more to this comment every time I get a little break from my Zoom training π I'll stop spamming you
When you say it's "used as justification", do you mean that to " extract code to a method once you've written it 3 times" isn't always a good idea? I'm trying to imagine a scenario where repeating oneself would be more efficient. Is this related to "prematurely optimizing"?
I really mean that the common misunderstanding is that DRY is about code, when it's actually about concepts. A single unit of information should be represented exactly once in your application. Now, most of the concepts and information in software are written in code, so DRY gets conflated with code. The guidelines are therefore useful βΒ most of the time if you have the same code 3 times you're most likely expressing the same idea and you want to extract it.
But sometimes you aren't expressing the same idea. And if you extract it to a shared piece of code, one or more things happen later:
Now you can probably just forget about all of that because it doesn't come up all that often, and experience will guide you.
To sum up my philosophy, I might invoke the idea of "self-documenting code."
So, as long as it reflects clear intent and concise (as possible) syntax with good naming conventions?
Sure! There's a lot of wiggle room in all of the details, and compromises need made in real life. It's ultimately subjective.
I got good news that I won't be getting laid off-- another department wants to hire me. That's come with a lot of training (8 hour zoom sessions daily), so I've been mentally drained this week. I'm planning a screen-free weekend to regroup and start fresh on Monday.
Hey, congrats! I'm sure that takes a weight off your shoulders.
That said though, I'd like to try item 5 before tomorrow evening! I'm sorry I haven't added much value to this repo yet, but watching you work has been really beneficial. This item looks like it should be simple!
No apologies necessary. This is all a learning process. I'm glad seeing some of it come to life has been helpful in some way.
Side note: painting the cells is strangely addicting, like popping packing bubbles.
lolol or doing ./bin/war_paint --rows 5 --cols 30
and writing letters like it's an old LCD screen.
I'm planning on making an array comprised of all Cell instances, and to iterate over it with a block that checks their state. If they're all filled, I'll have it output something like "GAME OVER!" and sleep a second before quitting.
Fortunately you already have the array of all Cell instances! The same one used to render the board can be used to manage everything else! Maybe something like @board.game_over?
:)
Hopefully that idea works! I'm adding more to this comment every time I get a little break from my Zoom training π I'll stop spamming you
π I only get one notification per new message, no worries! :)
Now you can probably just forget about all of that because it doesn't come up all that often, and experience will guide you.
Sounds good-- hopefully it'll all be second nature. I want to increase my reading comprehension.
Hey, congrats! I'm sure that takes a weight off your shoulders.
In some ways, yes! I'm very grateful because it looks like I'll get to keep working at home. Although, it looks like my workload doubled. Not a big deal at all, it just means I won't have as much study time during the day unfortunately. I'm going to start getting up an hour earlier to compensate.
lolol or doing ./bin/war_paint --rows 5 --cols 30 and writing letters like it's an old LCD screen.
I am not sure what that means! Haha, I tried running that and it just distorted the output?
Fortunately you already have the array of all Cell instances! The same one used to render the board can be used to manage everything else! Maybe something like @board.game_over? :)
Thanks for the reminder! I was able to get that working, and merged with one bug-- I couldn't get the last cell to paint before the "GAME OVER!" screen exited the program. After merging, I realized the last cell wasn't rendering because I placed game_over?
at the end of the main loop, ending before it could update the output. I moved it to the beginning to fix that and re-merged π
I'm checking the item off, but let me know how you would have done it differently!
lolol or doing ./bin/war_paint --rows 5 --cols 30 and writing letters like it's an old LCD screen.
I am not sure what that means! Haha, I tried running that and it just distorted the output?
On the output side, it's going to distort if you try to put more on screen than fits βΒ it just keeps drawing and wrapping even if it doesn't fit. That's the cause of that; I just had a wider terminal setting.
I didn't really mean LCD screen; rather dot matrix LED display. π
I'm checking the item off, but let me know how you would have done it differently!
Hey, grats on the feature and debugging it! I think that's a variant of a common game bug: more or less, does x need to happen on this frame or the next one?
Hmm, what would I have done differently? I think your board.game_over?
is spot on with all?
. Flattening is a fun touch!
There is a shortcut for sending methods to be used in blocks like that; it's idiomatic to use it when possible; that would be using the &
shortcut for Symbol#to_proc
:
@grid.flatten.all?(&:painted?)
You can read about it in the Ruby book in Chapter 5's subsection "Passing Block Arguments".
I also would consider pulling out some of the "work" about game over to a method; something like
if @board.game_over?
show_game_over_screen
break
end
But it's all fine as is!
Maybe eventually we'll add animations for game over or something. :)
This ticket is probably done then! A big chunk of work. Now we need to come up with iteration 2: maybe something that makes it feel more like a game!
Ahoy there! Long time no see.
I didn't really mean LCD screen; rather dot matrix LED display. laughing
Gotcha gotcha π
There is a shortcut for sending methods to be used in blocks like that; it's idiomatic to use it when possible; that would be using the & shortcut for Symbol#to_proc:
Radical! I wasn't aware of that shortcut but I tried it out now. This qualifies as the Ruby "syntactic sugar" I've read about, right? I made a PR and merged it.
I also would consider pulling out some of the "work" about game over to a method
Word! I think that helps it feel more organized. Made a second PR and merged it (working on committing more "atomic" changes--something I was reading about).
This ticket is probably done then! A big chunk of work. Now we need to come up with iteration 2: maybe something that makes it feel more like a game!
Eventually we'll need an AI that can compete with the player. And, we'll need to display another color on the screen that can compete for the cells. I think both sides of the board should be allowed to overtake painted turf to fuel competition and add value to special attacks / blocks. That would require changing the Game Over trigger to a new condition--I'm thinking maybe a stop clock?
So what about:
Let me know if you have any suggestions on where we go next with this π
Radical! I wasn't aware of that shortcut but I tried it out now. This qualifies as the Ruby "syntactic sugar" I've read about, right? I made a PR and merged it.
I think it would count as that, yes!
I think there's also some history there with Rails, where it might have even originated, at least with respect to symbols. But I can't find definitive history.
Made a second PR and merged it (working on committing more "atomic" changes--something I was reading about).
That's great! You'll find your own balance there. Git is its own beast and requires its own skillset, and some people care a lot about doing it certain ways, and some people don't care at all as long as the code makes it in. I'm closer to the former, but I have moments of the latter βΒ especially on personal projects.
Let me know if you have any suggestions on where we go next with this
Well... hmm. I think we might want to delay the hard part for just a tiny bit longer. The hard part will be finding some way to make it fun and engaging and strategic.
But yes, I think something like this:
@painted
as a boolean something like @painted_by
as :player
or :cpu
; { player: PAINTED_RED_COLOR, cpu: PAINTED_BLUE_COLOR }
I would start there without any stop clock stuff yet βΒ these are big enough changes and are prerequisites. This is maybe even worth an Iteration 2 ticket! π
I would start there without any stop clock stuff yet β these are big enough changes and are prerequisites. This is maybe even worth an Iteration 2 ticket! laughing
That's fair! I went ahead updated the wiki / opened the Iteration 2 issue. Let me know if it doesn't look right to you π
Maybe a map of colors in the board e.g. { player: PAINTED_RED_COLOR, cpu: PAINTED_BLUE_COLOR }
I wondered if you could clarify this for me? I think you mean mapping the color constant (from curses) to the board, with two keys (player / cpu)-- is that right?
Maybe a map of colors in the board e.g. { player: PAINTED_RED_COLOR, cpu: PAINTED_BLUE_COLOR }
I wondered if you could clarify this for me? I think you mean mapping the color constant (from curses) to the board, with two keys (player / cpu)-- is that right?
Yes βΒ although I think it might be in the renderer rather than the board. :) So that the board could say who painted the cell, and the renderer knows how to paint it via looking up in a hash map color = COLORS[cell.painted_by]
.
Alright, on to the next iteration!
Alright, what's next?
For big thinking and organizing big thinking, do we want something like a design doc where we can note what we've decided on what the game is and how it's played? Wiki page? https://github.com/erikamaker/war-paint/wiki
For baby step implementation, we should figure out some next steps. I like to sketch out ideas of what data is necessary and what types might be useful.
(In both cases, everything changes along the way, but I find writing something down can help me get started.)
One thing we may want to think about is separating the visualization from the game mechanics to whatever extent is possible. e.g. a
Board
contains the logical information about the game state, whereas aCursesBoardRenderer.new(board)
knows how to render it. (Also, I always just type whatever names pop in my head so that I'm not stuck; they're always changeable.)So maybe as a next step something like:
and make it show up? π