erikamaker / war-paint

Terminal turf-tagging game.
The Unlicense
0 stars 0 forks source link

Iteration 1 #4

Closed ianterrell closed 1 year ago

ianterrell commented 1 year ago

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 a CursesBoardRenderer.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:

board = Board.new rows: 7, cols: 7 # configurable?
renderer = CursesBoardRenderer.new board

and make it show up? πŸ˜…

erikamaker commented 1 year ago

> 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

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) 

⬚ ⬚ ⬚ ⬚ 
⬚ ⬚ ⬚ ⬚ 
⬚ ⬚ ⬚ ⬚ 
⬚ ⬚ ⬚ ⬚ 
ianterrell commented 1 year ago

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:

  1. run the game
  2. a grid shows up
  3. arrow keys to move around the grid
  4. enter or space to paint
  5. game detects when all spaces are filled
  6. you win!
  7. game exits

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?

erikamaker commented 1 year ago

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.

ianterrell commented 1 year ago

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:

  1. run the game
    • [x] partially done, you can run something
    • [x] initialize the board and renderer and start a game loop
    • [x] part of game loop is registering input
    • [x] accept q as input for now to quit
  2. a grid shows up
    • [x] part of game loop is rendering the board, so render it with curses
  3. arrow keys to move around the grid
    • [x] accept arrow keys in game loop input step
    • [x] track and update current position (probably a renderer responsibility)
    • [x] need a way to display which grid item you have selected
    • [x] change display based on current position
  4. enter or space to paint
    • [x] add this to input step
    • [x] renderer tells board it's been selected
  5. game detects when all spaces are filled
    • [x] part of loop is checking end criteria
  6. you win!
  7. game exits

Do 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.

erikamaker commented 1 year ago

I uploaded a file 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$ 
ianterrell commented 1 year ago

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:

Screenshot 2023-03-29 at 12 13 50 AM

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.

erikamaker commented 1 year ago

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:

image

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 πŸ˜„

ianterrell commented 1 year ago

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!

erikamaker commented 1 year ago

> You're learning a ton of different things all at once*; if it were me it would be bound to be overwhelming sometimes.

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?

ianterrell commented 1 year ago

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.

erikamaker commented 1 year ago

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?

ianterrell commented 1 year ago

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! :)

https://github.com/erikamaker/war-paint/blob/085947d48afa97f147f4429fe195ef00fdafaaf1/lib/curses_renderer.rb#L20

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?

erikamaker commented 1 year ago

> I think some of it was partially merged; I've just pushed to main with all my updates from last weekend.

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! πŸ˜„

erikamaker commented 1 year ago

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.

ianterrell commented 1 year ago

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:

https://github.com/erikamaker/war-paint/blob/425206f256ef948c70132e5acf02c62b5e3c5db1/Rakefile#L9-L12

I configured the executable to only run the curses based renderer if passed the short flag -x or long flag --curses:

https://github.com/erikamaker/war-paint/blob/425206f256ef948c70132e5acf02c62b5e3c5db1/bin/war_paint#L13-L15

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? :)

erikamaker commented 1 year ago

> EDIT: No, apparently I had changed something locally that evening to get it working with capital letters, and I didn't even remember. laughing

 -      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

  1. Removing the TerminalRenderer and any snippets that might trigger an error
  2. Getting the program to run again using the only version it can try
  3. Adding as much as I can to step 4. I have some ideas that I'm hoping will work.
ianterrell commented 1 year ago

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!

erikamaker commented 1 year ago

Got the appropriate program running using the manual path ./bin/war_paint --curses.

image

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.

  1. 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.

ianterrell commented 1 year ago

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.

Screenshot 2023-04-04 at 11 08 01 PM

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.

Screenshot 2023-04-04 at 11 09 25 PM

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.

erikamaker commented 1 year ago

> tada 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 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.

ianterrell commented 1 year ago

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:

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:

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?

erikamaker commented 1 year ago

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...

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 πŸ˜„

ianterrell commented 1 year ago

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!

erikamaker commented 1 year ago

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?

ianterrell commented 1 year ago

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.

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:

Screenshot 2023-04-09 at 10 16 11 AM

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.

erikamaker commented 1 year ago

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" πŸ˜…

ianterrell commented 1 year ago

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:

  1. Is it the most readable and clear?
  2. Is it self documenting?
  3. Is it DRY?
  4. Is it too complex?

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.

Screenshot 2023-04-11 at 9 24 11 AM

When you merged it, it merged into erika_test and not into main. I've pushed it all to main now.

erikamaker commented 1 year ago

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

ianterrell commented 1 year ago

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! :)

erikamaker commented 1 year ago

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!

ianterrell commented 1 year ago

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. πŸ˜†

Screenshot 2023-04-14 at 11 25 25 PM

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!

erikamaker commented 1 year ago

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:

  1. Add a second color (maybe blue?)
  2. A 1 minute stop clock that triggers Game Over when it reaches 0
  3. An AI class to compete with the player

Let me know if you have any suggestions on where we go next with this πŸ˜„

ianterrell commented 1 year ago

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:

  1. A class to represent the computer's work β€”Β I like the name CPU for fun because somewhere in the back of my head some games I played used that as the name of computer players, but anything is fine.
  2. Extend the board and grid to include who marks the cell, like rather than @painted as a boolean something like @painted_by as :player or :cpu;
  3. Adjust the loop to ask the player for their move and then the CPU for their move. The CPU can, for now, just pick the first empty cell it finds (or if you like, first empty or red).
  4. Maybe a map of colors in the board e.g. { 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! πŸ˜†

erikamaker commented 1 year ago

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?

ianterrell commented 1 year ago

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!