Untitled

 avatar
unknown
plain_text
11 days ago
9.4 kB
2
Indexable
import gleam/list
import gleam/option.{None, Some}
import lustre
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
import lustre/event

// All Types

pub type Disc {
  Red
  Yellow
  Empty
}

pub type Column =
  List(Disc)

pub type Board =
  List(Column)

pub type Player {
  PlayerRed
  PlayerYellow
}

pub type Model {
  Model(
    board: Board,
    current_player: Player,
    winner: option.Option(Player),
    theme: Theme,
  )
}

pub type Msg {
  ClickColumn(Int)
  Restart
  SetTheme(Theme)
}

pub type Theme {
  Christmas
  NewYear
  Space
}

// Set up the board

pub fn construct_board() -> Board {
  // creates a grid of 7 columns and 6 rows filled with Empty slots
  list.repeat(Empty, times: 6)
  |> fn(column) { list.repeat(column, times: 7) }
}

// columns are counted from 0 to 6 from left to right with the leftmost being number 0
// rows are counted from 0 to 5 from top to bottom with the topmost being number 0
//
// board = [[r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//          [r0, r1, r2, r3, r4, r5],
//         ]
//
// board structure:
// column 0: [r0, r1, r2, r3, r4, r5]
// column 1: [r0, r1, r2, r3, r4, r5]
// column 2: [r0, r1, r2, r3, r4, r5]
// column 3: [r0, r1, r2, r3, r4, r5]
// column 4: [r0, r1, r2, r3, r4, r5]
// column 5: [r0, r1, r2, r3, r4, r5]
// column 6: [r0, r1, r2, r3, r4, r5]
//
// visual
//         col_0 | col_1 | col_2 | col_3 | col_4 | col_5 | col_6
//         ______________________________________________________
// row_0  |c0_r0 | c1_r0 | c2_r0 | c3_r0 | c4_r0 | c5_r0 | c6_r0 |
// row_0  |c0_r1 | c1_r1 | c2_r1 | c3_r1 | c4_r1 | c5_r1 | c6_r1 |
// row_0  |c0_r2 | c1_r2 | c2_r2 | c3_r2 | c4_r2 | c5_r2 | c6_r2 |
// row_0  |c0_r3 | c1_r3 | c2_r3 | c3_r3 | c4_r3 | c5_r3 | c6_r3 |
// row_0  |c0_r4 | c1_r4 | c2_r4 | c3_r4 | c4_r4 | c5_r4 | c6_r4 |
// row_0  |c0_r5 | c1_r5 | c2_r5 | c3_r5 | c4_r5 | c5_r5 | c6_r5 |

fn init(_flags) -> Model {
  let board = construct_board()
  Model(board: board, current_player: PlayerRed, winner: None, theme: Christmas)
}

// Update 

fn update(model: Model, msg: Msg) -> Model {
  case msg {
    Restart -> init(Nil)

    ClickColumn(col_index) ->
      case model.winner {
        Some(_) -> model
        None -> handle_turn(model, col_index)
      }

    SetTheme(theme) ->
      Model(
        board: model.board,
        current_player: model.current_player,
        winner: model.winner,
        theme: theme,
      )
  }
}

fn handle_turn(model: Model, col_index: Int) -> Model {
  let disc = case model.current_player {
    PlayerRed -> Red
    PlayerYellow -> Yellow
  }

  let updated_board = update_board(model.board, col_index, disc)
  let has_won = check_for_win(updated_board, disc)

  let next_player = case has_won {
    True -> model.current_player
    False -> toggle_player(model.current_player)
  }

  let winner = case has_won {
    True -> Some(model.current_player)
    False -> None
  }

  Model(
    board: updated_board,
    current_player: next_player,
    winner: winner,
    theme: model.theme,
  )
}

fn toggle_player(player: Player) -> Player {
  case player {
    PlayerRed -> PlayerYellow
    PlayerYellow -> PlayerRed
  }
}

fn render_disc(disc: Disc) -> Element(Msg) {
  let class = case disc {
    Red -> "disc red"
    Yellow -> "disc yellow"
    Empty -> "disc empty"
  }

  html.div([attribute.class(class)], [])
}

fn render_column(index: Int, column: Column) -> Element(Msg) {
  let disc_elements =
    list.reverse(column)
    |> list.map(render_disc)

  html.div(
    [attribute.class("column"), event.on_click(ClickColumn(index))],
    disc_elements,
  )
}

// View

fn view(model: Model) -> Element(Msg) {
  let theme_class = case model.theme {
    Christmas -> "theme-christmas"
    NewYear -> "theme-newyear"
    Space -> "theme-space"
  }

  let column_elements =
    list.index_map(model.board, fn(column, index) {
      render_column(index, column)
    })

  let status_text = case model.winner {
    Some(PlayerRed) -> "Red wins!"
    Some(PlayerYellow) -> "Yellow wins!"
    None ->
      case model.current_player {
        PlayerRed -> "Red's turn"
        PlayerYellow -> "Yellow's turn"
      }
  }

  html.div([attribute.class("game-container " <> theme_class)], [
    html.h1([attribute.class("banner")], [html.text("Vier op een rij!")]),
    html.h2([attribute.class("status-text")], [html.text(status_text)]),
    html.div([attribute.class("board")], column_elements),
    html.button([event.on_click(Restart)], [html.text("Restart")]),
    render_theme_picker(),
  ])
}

// Themes
fn render_theme_picker() -> Element(Msg) {
  html.div([attribute.class("theme-picker")], [
    html.button([event.on_click(SetTheme(Christmas))], [html.text("Christmas")]),
    html.button([event.on_click(SetTheme(NewYear))], [html.text("New Year")]),
    html.button([event.on_click(SetTheme(Space))], [html.text("Space")]),
  ])
}

// Main Function

pub fn main() {
  let app = lustre.simple(init, update, view)
  let assert Ok(_) = lustre.start(app, "#app", Nil)
  Nil
}

// Placing discs
// Placing discs needs two functions: place_disc and update_board

pub fn place_disc(column: Column, disc: Disc) -> Column {
  case column {
    [_, _, _, _, _, Empty] -> [Empty, Empty, Empty, Empty, Empty, disc]
    [_, _, _, _, Empty, r5] -> [Empty, Empty, Empty, Empty, disc, r5]
    [_, _, _, Empty, r4, r5] -> [Empty, Empty, Empty, disc, r4, r5]
    [_, _, Empty, r3, r4, r5] -> [Empty, Empty, disc, r3, r4, r5]
    [_, Empty, r2, r3, r4, r5] -> [Empty, disc, r2, r3, r4, r5]
    [Empty, r1, r2, r3, r4, r5] -> [disc, r1, r2, r3, r4, r5]
    _ -> column
  }
}

pub fn update_board(board: Board, column_number: Int, disc: Disc) -> Board {
  case board {
    // Looks for the lowest Empty spot and puts the disc there
    [c0, ..tail] if column_number == 0 -> [place_disc(c0, disc), ..tail]
    [c0, c1, ..tail] if column_number == 1 -> [c0, place_disc(c1, disc), ..tail]
    [c0, c1, c2, ..tail] if column_number == 2 -> [
      c0,
      c1,
      place_disc(c2, disc),
      ..tail
    ]
    [c0, c1, c2, c3, ..tail] if column_number == 3 -> [
      c0,
      c1,
      c2,
      place_disc(c3, disc),
      ..tail
    ]
    [c0, c1, c2, c3, c4, ..tail] if column_number == 4 -> [
      c0,
      c1,
      c2,
      c3,
      place_disc(c4, disc),
      ..tail
    ]
    [c0, c1, c2, c3, c4, c5, ..tail] if column_number == 5 -> [
      c0,
      c1,
      c2,
      c3,
      c4,
      place_disc(c5, disc),
      ..tail
    ]
    [c0, c1, c2, c3, c4, c5, c6] if column_number == 6 -> [
      c0,
      c1,
      c2,
      c3,
      c4,
      c5,
      place_disc(c6, disc),
    ]
    _ -> board
  }
}

pub fn get_disc(board: Board, column_number: Int, row_number: Int) -> Disc {
  case list.drop(board, column_number) {
    [column, ..] ->
      case list.drop(column, row_number) {
        [disc, ..] -> disc
        [] -> Empty
      }
    [] -> Empty
  }
}

// The functions checks all the win conditions below.
pub fn check_for_win(board: Board, disc: Disc) -> Bool {
  check_for_win_vertical(board, disc)
  || check_for_win_horizontal(board, disc)
  || check_for_win_diagonal_up(board, disc)
  || check_for_win_diagonal_down(board, disc)
}

// We need functions for all win conditions.

pub fn check_for_win_vertical(board: Board, disc: Disc) -> Bool {
  let col_range = list.range(0, 6)
  let row_range = list.range(0, 2)
  list.any(col_range, fn(col) {
    list.any(row_range, fn(row) {
      get_disc(board, col, row) == disc
      && get_disc(board, col, row + 1) == disc
      && get_disc(board, col, row + 2) == disc
      && get_disc(board, col, row + 3) == disc
    })
  })
}

pub fn check_for_win_horizontal(board: Board, disc: Disc) -> Bool {
  let col_range = list.range(0, 3)
  let row_range = list.range(0, 5)
  list.any(col_range, fn(col) {
    list.any(row_range, fn(row) {
      get_disc(board, col, row) == disc
      && get_disc(board, col + 1, row) == disc
      && get_disc(board, col + 2, row) == disc
      && get_disc(board, col + 3, row) == disc
    })
  })
}

pub fn check_for_win_diagonal_down(board: Board, disc: Disc) -> Bool {
  let col_range = list.range(0, 3)
  let row_range = list.range(0, 2)
  list.any(col_range, fn(col) {
    list.any(row_range, fn(row) {
      get_disc(board, col, row) == disc
      && get_disc(board, col + 1, row + 1) == disc
      && get_disc(board, col + 2, row + 2) == disc
      && get_disc(board, col + 3, row + 3) == disc
    })
  })
}

pub fn check_for_win_diagonal_up(board: Board, disc: Disc) -> Bool {
  let col_range = list.range(0, 3)
  let row_range = list.range(3, 5)
  list.any(col_range, fn(col) {
    list.any(row_range, fn(row) {
      get_disc(board, col, row) == disc
      && get_disc(board, col + 1, row - 1) == disc
      && get_disc(board, col + 2, row - 2) == disc
      && get_disc(board, col + 3, row - 3) == disc
    })
  })
}
Editor is loading...
Leave a Comment