- commit
- 4c7d08e2948ccaca091db4f1ef31f5fc33030079
- parent
- 400a626648e61f80d33409ddfe966c12e26cc000
- author
- Dami <dami.etoile@gmail.com>
| M | model.go | 2 | + |
| M | resolve.go | 3 | + |
| A | takuzu/ | 93 | ++++++++++++++ |
| A | takuzu/ | 48 | +++++++ |
| A | takuzu/ | 164 | +++++++++++++++++++++++++ |
| A | takuzu/ | 195 | ++++++++++++++++++++++++++++++ |
| A | takuzu/ | 56 | +++++++++ |
| A | takuzu/ | 52 | ++++++++ |
| A | takuzu/ | 110 | +++++++++++++++++ |
| A | vhs/ | 65 | ++++++++++ |
@@ -13,6 +13,7 @@ import (
1313 "github.com/FelineStateMachine/puzzletea/nonogram"1414 "github.com/FelineStateMachine/puzzletea/store"1515 "github.com/FelineStateMachine/puzzletea/sudoku"16+ "github.com/FelineStateMachine/puzzletea/takuzu"1617 "github.com/FelineStateMachine/puzzletea/wordsearch"1718 "github.com/charmbracelet/bubbles/list"1819 "github.com/charmbracelet/bubbles/table"@@ -38,6 +39,7 @@ var (
3839 game.Category{Name: "Lights Out", Desc: "Turn off all the lights.", Modes: lightsout.Modes},3940 game.Category{Name: "Nonogram", Desc: "Fill cells to match row and column hints.", Modes: nonogram.Modes},4041 game.Category{Name: "Sudoku", Desc: "Fill the 9x9 grid following sudoku rules.", Modes: sudoku.Modes},42+ game.Category{Name: "Takuzu", Desc: "Fill the grid with ● and ○.", Modes: takuzu.Modes},4143 game.Category{Name: "Word Search", Desc: "Find hidden words in a letter grid.", Modes: wordsearch.Modes},4244 }4345 )@@ -0,0 +1,93 @@
1+package takuzu2+3+import (4+ "encoding/json"5+ "fmt"6+ "strings"7+8+ "github.com/FelineStateMachine/puzzletea/game"9+)10+11+// Save represents the serialized state of a Takuzu game.12+type Save struct {13+ Size int `json:"size"`14+ State string `json:"state"`15+ Provided string `json:"provided"`16+ ModeTitle string `json:"mode_title"`17+ Solved bool `json:"solved"`18+}19+20+func (m Model) GetSave() ([]byte, error) {21+ save := Save{22+ Size: m.size,23+ State: m.grid.String(),24+ Provided: serializeProvided(m.provided),25+ ModeTitle: m.modeTitle,26+ Solved: m.solved,27+ }28+ data, err := json.Marshal(save)29+ if err != nil {30+ return nil, fmt.Errorf("failed to marshal takuzu save: %w", err)31+ }32+ return data, nil33+}34+35+// ImportModel reconstructs a Model from saved JSON data.36+func ImportModel(data []byte) (*Model, error) {37+ var s Save38+ if err := json.Unmarshal(data, &s); err != nil {39+ return nil, fmt.Errorf("failed to unmarshal takuzu save: %w", err)40+ }41+ if s.Size <= 0 {42+ return nil, fmt.Errorf("invalid grid size in save: %d", s.Size)43+ }44+45+ g := newGrid(state(s.State))46+ provided := deserializeProvided(s.Provided, s.Size)47+48+ m := &Model{49+ size: s.Size,50+ grid: g,51+ provided: provided,52+ cursor: game.Cursor{X: 0, Y: 0},53+ keys: DefaultKeyMap,54+ modeTitle: s.ModeTitle,55+ }56+ m.solved = m.checkSolved()57+ return m, nil58+}59+60+// serializeProvided encodes the provided matrix as a string of '.' (not provided) and '#' (provided).61+func serializeProvided(p [][]bool) string {62+ var b strings.Builder63+ for y, row := range p {64+ for _, v := range row {65+ if v {66+ b.WriteByte('#')67+ } else {68+ b.WriteByte('.')69+ }70+ }71+ if y < len(p)-1 {72+ b.WriteByte('\n')73+ }74+ }75+ return b.String()76+}77+78+// deserializeProvided decodes a provided-matrix string back into [][]bool.79+func deserializeProvided(s string, size int) [][]bool {80+ rows := strings.Split(s, "\n")81+ p := make([][]bool, size)82+ for y := range size {83+ p[y] = make([]bool, size)84+ if y < len(rows) {85+ for x := range size {86+ if x < len(rows[y]) && rows[y][x] == '#' {87+ p[y][x] = true88+ }89+ }90+ }91+ }92+ return p93+}@@ -0,0 +1,48 @@
1+package takuzu2+3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/list"6+)7+8+func init() {9+ game.Register("Takuzu", func(data []byte) (game.Gamer, error) {10+ return ImportModel(data)11+ })12+}13+14+// TakuzuMode represents a specific difficulty configuration for Takuzu.15+type TakuzuMode struct {16+ game.BaseMode17+ Size int18+ Prefilled float6419+}20+21+var (22+ _ game.Mode = TakuzuMode{}23+ _ game.Spawner = TakuzuMode{}24+)25+26+// NewMode creates a new Takuzu game mode.27+func NewMode(title, desc string, size int, prefilled float64) TakuzuMode {28+ return TakuzuMode{29+ BaseMode: game.NewBaseMode(title, desc),30+ Size: size,31+ Prefilled: prefilled,32+ }33+}34+35+// Spawn creates a new game instance for this mode.36+func (t TakuzuMode) Spawn() (game.Gamer, error) {37+ complete := generateComplete(t.Size)38+ puzzle, provided := generatePuzzle(complete, t.Size, t.Prefilled)39+ return New(t, puzzle, provided)40+}41+42+// Modes defines the available difficulty levels.43+var Modes = []list.Item{44+ NewMode("Easy", "6×6 grid, ~50% clues.", 6, 0.50),45+ NewMode("Medium", "8×8 grid, ~40% clues.", 8, 0.40),46+ NewMode("Hard", "10×10 grid, ~35% clues.", 10, 0.35),47+ NewMode("Expert", "12×12 grid, ~30% clues.", 12, 0.30),48+}@@ -0,0 +1,164 @@
1+// Package takuzu implements the binary (Binairo) puzzle game.2+package takuzu3+4+import (5+ "fmt"6+7+ "github.com/FelineStateMachine/puzzletea/game"8+ "github.com/charmbracelet/bubbles/key"9+ tea "github.com/charmbracelet/bubbletea"10+ "github.com/charmbracelet/lipgloss"11+)12+13+// Model implements game.Gamer for Takuzu.14+type Model struct {15+ size int16+ grid grid17+ provided [][]bool18+ cursor game.Cursor19+ solved bool20+ keys KeyMap21+ modeTitle string22+ showFullHelp bool23+}24+25+var _ game.Gamer = Model{}26+27+// New creates a new Takuzu game model.28+func New(mode TakuzuMode, puzzle grid, provided [][]bool) (game.Gamer, error) {29+ m := Model{30+ size: mode.Size,31+ grid: puzzle,32+ provided: provided,33+ cursor: game.Cursor{X: 0, Y: 0},34+ keys: DefaultKeyMap,35+ modeTitle: mode.Title(),36+ }37+ m.solved = m.checkSolved()38+ return m, nil39+}40+41+func (m Model) Init() tea.Cmd {42+ return nil43+}44+45+func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {46+ switch msg := msg.(type) {47+ case game.HelpToggleMsg:48+ m.showFullHelp = msg.Show49+ case tea.KeyMsg:50+ switch {51+ case key.Matches(msg, m.keys.PlaceZero):52+ if !m.provided[m.cursor.Y][m.cursor.X] && !m.solved {53+ m.grid[m.cursor.Y][m.cursor.X] = zeroCell54+ m.solved = m.checkSolved()55+ }56+ case key.Matches(msg, m.keys.PlaceOne):57+ if !m.provided[m.cursor.Y][m.cursor.X] && !m.solved {58+ m.grid[m.cursor.Y][m.cursor.X] = oneCell59+ m.solved = m.checkSolved()60+ }61+ case key.Matches(msg, m.keys.Clear):62+ if !m.provided[m.cursor.Y][m.cursor.X] && !m.solved {63+ m.grid[m.cursor.Y][m.cursor.X] = emptyCell64+ m.solved = m.checkSolved()65+ }66+ default:67+ m.cursor.Move(m.keys.CursorKeyMap, msg, m.size-1, m.size-1)68+ }69+ }70+ m.updateKeyBindings()71+ return m, nil72+}73+74+func (m Model) View() string {75+ title := game.TitleBarView("Takuzu", m.modeTitle, m.solved)76+ grid := gridView(m.grid, m.provided, m.cursor, m.solved)77+ status := statusBarView(m.showFullHelp)78+ return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)79+}80+81+func (m Model) SetTitle(t string) game.Gamer {82+ m.modeTitle = t83+ return m84+}85+86+func (m Model) IsSolved() bool {87+ return m.solved88+}89+90+func (m Model) checkSolved() bool {91+ size := m.size92+ half := size / 293+94+ // All cells must be filled.95+ for y := range size {96+ for x := range size {97+ if m.grid[y][x] == emptyCell {98+ return false99+ }100+ }101+ }102+103+ for i := range size {104+ zeroRow, oneRow := 0, 0105+ zeroCol, oneCol := 0, 0106+ for j := range size {107+ // Count in row.108+ switch m.grid[i][j] {109+ case zeroCell:110+ zeroRow++111+ case oneCell:112+ oneRow++113+ }114+ // Count in column.115+ switch m.grid[j][i] {116+ case zeroCell:117+ zeroCol++118+ case oneCell:119+ oneCol++120+ }121+122+ // Check no three consecutive in row.123+ if j >= 2 && m.grid[i][j] == m.grid[i][j-1] && m.grid[i][j] == m.grid[i][j-2] {124+ return false125+ }126+ // Check no three consecutive in column.127+ if j >= 2 && m.grid[j][i] == m.grid[j-1][i] && m.grid[j][i] == m.grid[j-2][i] {128+ return false129+ }130+ }131+ if zeroRow != half || oneRow != half {132+ return false133+ }134+ if zeroCol != half || oneCol != half {135+ return false136+ }137+ }138+139+ // Unique rows and columns.140+ return hasUniqueLines(m.grid, size)141+}142+143+func (m Model) GetDebugInfo() string {144+ status := "In Progress"145+ if m.solved {146+ status = "Solved"147+ }148+149+ filled := 0150+ for y := range m.size {151+ for x := range m.size {152+ if m.grid[y][x] != emptyCell {153+ filled++154+ }155+ }156+ }157+158+ return game.DebugHeader("Takuzu", [][2]string{159+ {"Status", status},160+ {"Cursor", fmt.Sprintf("(%d, %d)", m.cursor.X, m.cursor.Y)},161+ {"Grid Size", fmt.Sprintf("%d×%d", m.size, m.size)},162+ {"Cells Filled", fmt.Sprintf("%d / %d", filled, m.size*m.size)},163+ })164+}@@ -0,0 +1,195 @@
1+package takuzu2+3+import "math/rand/v2"4+5+// generateComplete fills an empty grid with a valid Takuzu solution using backtracking.6+func generateComplete(size int) grid {7+ g := newGrid(createEmptyState(size))8+ fillGrid(g, size)9+ return g10+}11+12+func fillGrid(g grid, size int) bool {13+ for y := range size {14+ for x := range size {15+ if g[y][x] != emptyCell {16+ continue17+ }18+ vals := [2]rune{zeroCell, oneCell}19+ if rand.IntN(2) == 0 {20+ vals[0], vals[1] = vals[1], vals[0]21+ }22+ for _, v := range vals {23+ if canPlace(g, size, x, y, v) {24+ g[y][x] = v25+ if fillGrid(g, size) {26+ return true27+ }28+ g[y][x] = emptyCell29+ }30+ }31+ return false32+ }33+ }34+ return true35+}36+37+// canPlace checks whether placing val at (x,y) would violate Takuzu constraints.38+func canPlace(g grid, size, x, y int, val rune) bool {39+ // Check no three consecutive in row.40+ if x >= 2 && g[y][x-1] == val && g[y][x-2] == val {41+ return false42+ }43+ if x >= 1 && x < size-1 && g[y][x-1] == val && g[y][x+1] == val {44+ return false45+ }46+ if x <= size-3 && g[y][x+1] == val && g[y][x+2] == val {47+ return false48+ }49+50+ // Check no three consecutive in column.51+ if y >= 2 && g[y-1][x] == val && g[y-2][x] == val {52+ return false53+ }54+ if y >= 1 && y < size-1 && g[y-1][x] == val && g[y+1][x] == val {55+ return false56+ }57+ if y <= size-3 && g[y+1][x] == val && g[y+2][x] == val {58+ return false59+ }60+61+ // Check count in row doesn't exceed size/2.62+ half := size / 263+ count := 064+ for _, c := range g[y] {65+ if c == val {66+ count++67+ }68+ }69+ if count >= half {70+ return false71+ }72+73+ // Check count in column doesn't exceed size/2.74+ count = 075+ for r := range size {76+ if g[r][x] == val {77+ count++78+ }79+ }80+ if count >= half {81+ return false82+ }83+84+ return true85+}86+87+// generatePuzzle removes cells from a complete grid to create a puzzle with a unique solution.88+func generatePuzzle(complete grid, size int, prefilled float64) (puzzle grid, provided [][]bool) {89+ puzzle = complete.clone()90+ provided = make([][]bool, size)91+ for y := range size {92+ provided[y] = make([]bool, size)93+ for x := range size {94+ provided[y][x] = true95+ }96+ }97+98+ target := int(prefilled * float64(size*size))99+100+ // Shuffled positions.101+ type pos struct{ x, y int }102+ positions := make([]pos, 0, size*size)103+ for y := range size {104+ for x := range size {105+ positions = append(positions, pos{x, y})106+ }107+ }108+ rand.Shuffle(len(positions), func(i, j int) {109+ positions[i], positions[j] = positions[j], positions[i]110+ })111+112+ filled := size * size113+ for _, p := range positions {114+ if filled <= target {115+ break116+ }117+ saved := puzzle[p.y][p.x]118+ puzzle[p.y][p.x] = emptyCell119+ if countSolutions(puzzle, size, 2) != 1 {120+ puzzle[p.y][p.x] = saved121+ } else {122+ provided[p.y][p.x] = false123+ filled--124+ }125+ }126+127+ return puzzle, provided128+}129+130+// countSolutions counts solutions of the grid up to limit using backtracking.131+func countSolutions(g grid, size, limit int) int {132+ for y := range size {133+ for x := range size {134+ if g[y][x] != emptyCell {135+ continue136+ }137+ count := 0138+ for _, v := range [2]rune{zeroCell, oneCell} {139+ if canPlace(g, size, x, y, v) {140+ g[y][x] = v141+ count += countSolutions(g, size, limit-count)142+ g[y][x] = emptyCell143+ if count >= limit {144+ return count145+ }146+ }147+ }148+ return count149+ }150+ }151+ // All cells filled — check uniqueness constraints.152+ if hasUniqueLines(g, size) {153+ return 1154+ }155+ return 0156+}157+158+// hasUniqueLines checks that all rows are unique and all columns are unique.159+func hasUniqueLines(g grid, size int) bool {160+ // Check rows.161+ for i := range size {162+ for j := i + 1; j < size; j++ {163+ if rowEqual(g[i], g[j]) {164+ return false165+ }166+ }167+ }168+ // Check columns.169+ for i := range size {170+ for j := i + 1; j < size; j++ {171+ if colEqual(g, size, i, j) {172+ return false173+ }174+ }175+ }176+ return true177+}178+179+func rowEqual(a, b []rune) bool {180+ for i := range a {181+ if a[i] != b[i] {182+ return false183+ }184+ }185+ return true186+}187+188+func colEqual(g grid, size, c1, c2 int) bool {189+ for r := range size {190+ if g[r][c1] != g[r][c2] {191+ return false192+ }193+ }194+ return true195+}@@ -0,0 +1,56 @@
1+package takuzu2+3+import (4+ "strings"5+)6+7+const (8+ emptyCell rune = '.'9+ zeroCell rune = '0'10+ oneCell rune = '1'11+)12+13+type (14+ grid [][]rune15+ state string16+)17+18+func newGrid(s state) grid {19+ rows := strings.Split(string(s), "\n")20+ g := make(grid, len(rows))21+ for i, row := range rows {22+ g[i] = []rune(row)23+ }24+ return g25+}26+27+func (g grid) String() string {28+ rows := make([]string, len(g))29+ for i, row := range g {30+ rows[i] = string(row)31+ }32+ return strings.Join(rows, "\n")33+}34+35+func createEmptyState(size int) state {36+ var b strings.Builder37+ b.Grow((size + 1) * size)38+ for y := range size {39+ for range size {40+ b.WriteRune(emptyCell)41+ }42+ if y < size-1 {43+ b.WriteRune('\n')44+ }45+ }46+ return state(b.String())47+}48+49+func (g grid) clone() grid {50+ c := make(grid, len(g))51+ for i, row := range g {52+ c[i] = make([]rune, len(row))53+ copy(c[i], row)54+ }55+ return c56+}@@ -0,0 +1,52 @@
1+package takuzu2+3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/key"6+)7+8+// KeyMap defines the keybindings for Takuzu.9+type KeyMap struct {10+ game.CursorKeyMap11+ PlaceZero key.Binding12+ PlaceOne key.Binding13+ Clear key.Binding14+}15+16+// DefaultKeyMap provides the standard keybindings.17+var DefaultKeyMap = KeyMap{18+ CursorKeyMap: game.DefaultCursorKeyMap,19+ PlaceZero: key.NewBinding(20+ key.WithKeys("z", "0"),21+ key.WithHelp("z/0", "Place ●"),22+ ),23+ PlaceOne: key.NewBinding(24+ key.WithKeys("x", "1"),25+ key.WithHelp("x/1", "Place ○"),26+ ),27+ Clear: key.NewBinding(28+ key.WithKeys("backspace", "delete"),29+ key.WithHelp("bkspc", "Clear"),30+ ),31+}32+33+func (m *Model) updateKeyBindings() {34+ m.keys.Up.SetEnabled(m.cursor.Y > 0)35+ m.keys.Down.SetEnabled(m.cursor.Y < m.size-1)36+ m.keys.Left.SetEnabled(m.cursor.X > 0)37+ m.keys.Right.SetEnabled(m.cursor.X < m.size-1)38+39+ onProvided := m.provided[m.cursor.Y][m.cursor.X]40+ solved := m.solved41+ m.keys.PlaceZero.SetEnabled(!onProvided && !solved)42+ m.keys.PlaceOne.SetEnabled(!onProvided && !solved)43+ m.keys.Clear.SetEnabled(!onProvided && !solved)44+}45+46+// GetFullHelp implements game.Gamer.47+func (m Model) GetFullHelp() [][]key.Binding {48+ return [][]key.Binding{49+ {m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right},50+ {m.keys.PlaceZero, m.keys.PlaceOne, m.keys.Clear},51+ }52+}@@ -0,0 +1,110 @@
1+package takuzu2+3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/lipgloss"6+)7+8+var (9+ baseStyle = lipgloss.NewStyle()10+11+ zeroStyle = baseStyle.12+ Foreground(lipgloss.AdaptiveColor{Light: "130", Dark: "222"}).13+ Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"})14+15+ oneStyle = baseStyle.16+ Foreground(lipgloss.AdaptiveColor{Light: "68", Dark: "111"}).17+ Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"})18+19+ emptyStyle = baseStyle.20+ Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "240"}).21+ Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"})22+23+ cursorStyle = baseStyle.24+ Bold(true).25+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).26+ Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})27+28+ crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}29+30+ gridBorderStyle = lipgloss.NewStyle().31+ Border(lipgloss.RoundedBorder()).32+ BorderForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "240"})33+34+ gridBorderSolvedStyle = lipgloss.NewStyle().35+ Border(lipgloss.RoundedBorder()).36+ BorderForeground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"})37+38+ statusBarStyle = lipgloss.NewStyle().39+ Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).40+ MarginTop(1)41+42+ cellWidth = 343+44+ renderStyleMap = map[rune]lipgloss.Style{45+ zeroCell: zeroStyle,46+ oneCell: oneStyle,47+ emptyCell: emptyStyle,48+ }49+50+ renderRuneMap = map[rune]string{51+ zeroCell: " ● ",52+ oneCell: " ○ ",53+ emptyCell: " · ",54+ }55+)56+57+func cellView(val rune, isProvided, isCursor, inCursorRow, inCursorCol, solved bool) string {58+ s, ok := renderStyleMap[val]59+ if !ok {60+ s = emptyStyle61+ }62+63+ if isProvided && val != emptyCell {64+ s = s.Bold(true)65+ }66+67+ if isCursor && !solved {68+ s = cursorStyle69+ } else if !solved && (inCursorRow || inCursorCol) {70+ s = s.Background(crosshairBG)71+ }72+73+ if solved {74+ s = s.Background(lipgloss.AdaptiveColor{Light: "151", Dark: "22"})75+ }76+77+ r, ok := renderRuneMap[val]78+ if !ok {79+ r = renderRuneMap[emptyCell]80+ }81+ return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)82+}83+84+func gridView(g grid, provided [][]bool, c game.Cursor, solved bool) string {85+ var rows []string86+ for y, row := range g {87+ var rowCells []string88+ for x, val := range row {89+ isCursor := x == c.X && y == c.Y90+ inCursorRow := y == c.Y91+ inCursorCol := x == c.X92+ cell := cellView(val, provided[y][x], isCursor, inCursorRow, inCursorCol, solved)93+ rowCells = append(rowCells, cell)94+ }95+ rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, rowCells...))96+ }97+ content := lipgloss.JoinVertical(lipgloss.Left, rows...)98+99+ if solved {100+ return gridBorderSolvedStyle.Render(content)101+ }102+ return gridBorderStyle.Render(content)103+}104+105+func statusBarView(showFullHelp bool) string {106+ if showFullHelp {107+ return statusBarStyle.Render("arrows/wasd: move z: ● x: ○ bkspc: clear ctrl+n: menu ctrl+h: help")108+ }109+ return statusBarStyle.Render("z: ● x: ○ bkspc: clear")110+}@@ -0,0 +1,65 @@
1+Output vhs/takuzu.gif2+3+Set FontSize 134+Set Width 8545+Set Height 4806+Set Padding 407+Set Theme "Catppuccin Mocha"8+9+Env COLORTERM "truecolor"10+Env CLICOLOR_FORCE "1"11+Env CI ""12+13+Hide14+Type "go build -o puzzletea"15+Enter16+Sleep 2s17+Ctrl+L18+Show19+20+Type "./puzzletea new takuzu easy"21+Sleep 500ms22+Enter23+Sleep 2s24+25+# Place a filled circle26+Type "z"27+Sleep 500ms28+29+# Move and place an open circle30+Right31+Sleep 300ms32+Type "x"33+Sleep 500ms34+35+# Move down and place another36+Down37+Sleep 300ms38+Type "z"39+Sleep 500ms40+41+# Demonstrate clearing42+Backspace43+Sleep 500ms44+45+# Re-place with opposite value46+Type "x"47+Sleep 500ms48+49+# Navigate around to show crosshair50+Down51+Down52+Sleep 300ms53+Right54+Right55+Sleep 300ms56+57+# Place a few more58+Type "z"59+Sleep 500ms60+Down61+Sleep 300ms62+Type "x"63+Sleep 500ms64+65+Sleep 3s